如何从存根函数参数获取属性? [英] How to get property from stub function argument?

查看:83
本文介绍了如何从存根函数参数获取属性?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个服务,该服务应创建一个电子邮件类对象并将其传递给第三类(电子邮件发件人).

我要检查由功能生成的电子邮件正文.

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);
}

因此,我为emailService和email创建了一个存根类.但是我无法验证电子邮件的正文.我也无法检查是否调用了$ email-> setBody(),因为电子邮件是在经过测试的函数内部创建的

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();
    }
}

我知道了:

Call to undefined method Prophecy\Prophecy\MethodProphecy::shouldBe() in /private/tmp/phpspec/spec/App/ServiceSpec.php on line 18 

在真实应用中,主体是生成的,所以我想测试一下它是否正确生成.我该怎么办?

解决方案

在PHPSpec中,您不能对创建的对象(甚至是在spec文件中创建的存根或模拟对象)进行这种断言:可以匹配的是SUS( S ystem U nder S pec)及其返回值(如果有).

我将写一些指南以使您的测试通过 ,并改善您的设计和可测试性


从我的角度来看有什么问题

Service

new的用法

为什么错了

Service有两个职责:创建一个Email对象并执行其工作. SOLID原则的SRP中断.而且,您失去了对对象创建的控制,这变得非常难以测试

通过规格的解决方法

我建议使用工厂(如下所示),因为可测试性会大大提高,但是在这种情况下,您可以通过按如下所示重写规范来使测试通过

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();
    }
}

只要setBody在SUS实施中不更改,就可以使用. 但是,我不建议这样做,因为从PHPSpec的角度来看这应该是一种气味.

使用工厂

创建工厂

class EmailFactory()
{
    public function createEmail($body)
    {
        $email = new Email();
        $email->setBody($body);

        return $email;
    }
}

及其规格

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);
    }
}

现在,您确定工厂的createEmail已完成您期望的工作.如您所知,责任被封装在这里,您无需担心其他地方(考虑一种可以选择如何发送邮件的策略:直接将邮件放入队列等;如果您使用原始方法解决它们,则需要在每种具体策略中测试是否已按预期创建了电子邮件,而现在则不需要).

将工厂整合到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);
    } 
}

最后通过规格(正确的方式)

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();
}

我没有尝试过这个例子,可能会有一些错别字,但是我100%肯定这种方法:希望所有读者都明白.

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.

这篇关于如何从存根函数参数获取属性?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆