I have a service, which should create an email class object and pass it to the third class (email-sender).
I want to check body of email, which generates by the function.
Service.php
class Service
{
/** @var EmailService */
protected $emailService;
public function __construct(EmailService $emailService)
{
$this->emailService = $emailService;
}
public function testFunc()
{
$email = new Email();
$email->setBody('abc'); // I want to test this attribute
$this->emailService->send($email);
}
}
Email.php:
class Email
{
protected $body;
public function setBody($body)
{
$this->body = $body;
}
public function getBody()
{
return $this->body;
}
}
EmailService.php
interface EmailService
{
public function send(Email $email);
}
So I create a stub class for emailService and email. But I can't validate the body of the email. I also can't check if $email->setBody() was called, because email is created inside the tested function
class ServiceSpec extends ObjectBehavior
{
function it_creates_email_with_body_abc(EmailService $emailService, Email $email)
{
$this->beConstructedWith($emailService);
$emailService->send($email);
$email->getBody()->shouldBe('abc');
$this->testFunc();
}
}
I got this:
Call to undefined method Prophecy\Prophecy\MethodProphecy::shouldBe() in /private/tmp/phpspec/spec/App/ServiceSpec.php on line 18
In real app the body is generated, so I want to test, if it's generated correctly. How can I do that?
In PHPSpec you can't make this kind of assertion onto created objects (and even on stubs or mocks as you create them in spec file): only thing you can match on are SUS (System Under Spec) and its returned value (if any).
I'm gonna write a little guide to make your test pass and to improve your design and testability
What's wrong from my point of view
new
usage inside Service
Why is wrong
Service
has two responsibility: create an Email
object and do its job. This break SRP of SOLID principles. Moreover you lost control over object creation and this become, as you spotted, very difficult to test
Workaround to make spec pass
I would recommend to use a factory (as I will show below) for this kind of task because increase testability dramatically but, in this case, you can make your test pass by rewriting the spec as follows
class ServiceSpec extends ObjectBehavior
{
function it_creates_email_with_body_abc(EmailService $emailService)
{
$this->beConstructedWith($emailService);
//arrange data
$email = new Email();
$email->setBody('abc');
//assert
$emailService->send($email)->shouldBeCalled();
//act
$this->testFunc();
}
}
As long as setBody
does not change in SUS implementation, this works.
However I would not recommend it as this should be a smell from PHPSpec point of view.
Use a factory
Create the factory
class EmailFactory()
{
public function createEmail($body)
{
$email = new Email();
$email->setBody($body);
return $email;
}
}
and its spec
public function EmailFactorySpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(EmailFactory::class);
}
function it_creates_email_with_body_content()
{
$body = 'abc';
$email = $this->createEmail($body);
$email->shouldBeAnInstanceOf(Email::class);
$email->getBody()->shouldBeEqualTo($body);
}
}
Now you're sure that createEmail
of the factory does what you expect to do. As you can notice, responsibility is incapsulated here and you don't need to worry elsewhere (think about a strategy where you can choose how to send mails: directly, putting them in a queue and so on; if you tackle them with original approach, you need to test in every concrete strategy that email is created as you expected whereas now you don't).
Integrate the factory in SUS
class Service
{
/** @var EmailService */
protected $emailService;
/** @var EmailFactory */
protected $emailFactory;
public function __construct(
EmailService $emailService, EmailFactory $emailFactory
) {
$this->emailService = $emailService;
$this->emailFactory = $emailFactory;
}
public function testFunc()
{
$email = $this->emailFactory->createEmail('abc');
$this->emailService->send($email);
}
}
Finally make spec pass (the right way)
function it_creates_email_with_body_abc(
EmailService $emailService, EmailFactory $emailFactory, Email $mail
) {
$this->beConstructedWith($emailService);
// if you need to be sure that body will be 'abc',
// otherwise you can use Argument::type('string') wildcard
$emailFactory->createEmail('abc')->willReturn($email);
$emailService->send($email)->shouldBeCalled();
$this->testFunc();
}
I did not tried myself this examples and there could be some typos but I'm 100% sure of this approach: I hope is clear for all readers.
来源:https://stackoverflow.com/questions/42307939/how-to-get-property-from-stub-function-argument