phptestingphpspec

How to get property from stub function argument?


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?


Solution

  • 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.