【问题标题】:how to mock a class and override a method如何模拟一个类并覆盖一个方法
【发布时间】:2018-08-03 06:47:29
【问题描述】:

我正在测试这个类,它在更深的地方使用 Redis:

<?php

class Publisher {
    function publish($message) {
        Redis::publish($message);
    }
}

class Foo {
    public function publishMessage() {
        $message = $this->generateMessage();
        $this->publish($message);
    }

    private function publish($message) {
        $this->getPublisher()->publish($message);
    }

    // below just for testing
    private $publisher;

    public function getPublisher() {
        if(empty($this->publisher) {
             return new Publisher();
        }
        return $this->publisher;
    }

    public function setPublisher($publisher) {
        $this->publisher = $publisher;
    }
}

现在我不知道如何测试这个。当然我不想测试 Redis。我真正需要测试的是发送到 Redis 的消息是否是我所期望的。 (我想) 我可以编写一个函数来返回消息并且是公开的。但我不喜欢这个主意。 在此示例中,我可以设置发布者,因此在测试时我可以返回另一个 Publisher 类。 它不会发送消息,而是将其保存在内部,以便我以后可以断言它。

class Publisher {
    public $message;
    function publish($message) {
        $this->message = $message;
    }
}  

但是我不知道如何模拟 Publisher 类来更改方法。或者我必须从 Publisher 类继承。 同样,我测试的类必须包含仅用于测试的代码。我也不喜欢。

我该如何正确测试呢? 存在 Redis 的模拟库,但不支持发布。

【问题讨论】:

  • 我没有得到答案,但你忘记分配 $message: class Publisher { public $message;函数发布($message){ $this->message = $message; } }
  • 感谢您的关注。

标签: php unit-testing redis mocking phpunit


【解决方案1】:

一些选项描述为测试类的方法

class FooTest extends PHPUnit_Framework_TestCase // or PHPUnit\Framework\TestCase for version
{

    /**
     * First option: with PHPUnit's MockObject builder.
     */
    public function testPublishMessageWithMockBuilder() {
        // Internally mock builder creates new class that extends your Publisher
        $publisherMock = $this
            ->getMockBuilder(Publisher::class)
            ->setMethods(['publish'])
            ->getMock();

        $publisherMock
            ->expects($this->any()) // how many times we expect our method to be called
            ->method('publish') // which method
            ->with($this->exactly('your expected message')) // with what parameters we expect method "publish" to be called
            ->willReturn('what should be returned');
        $testedObject = new Foo;
        $testedObject->setPublisher($publisherMock);
        $testedObject->publish();
    }

    /**
     * Second option: with Prophecy
     */
    public function testPublishMessageWithProphecy() {
        // Internally prophecy creates new class that extends your Publisher
        $publisherMock = $this->prophesize(Publisher::class);

        // assert that publish should be called with parameters
        $publisherMock
            ->publish('expected message')
            ->shouldBeCalled();

        $testedObject = new Foo;
        $testedObject->setPublisher($publisherMock->reveal());
        $testedObject->publish();
    }

    /**
     * Third wierd option: with anonymous class (php version >= 7)
     * I am not recommend do something like that, its just for example
     */
    public function testFooWithAnonymousClass()
    {
        // explicitly extend stubbed class and overwrite method "publish"
        $publisherStub = new class () extends Publisher {
            public function publish($message)
            {
                assert($message === 'expexted message');
            }
        };
        $testedObject = new Foo;
        $testedObject->setPublisher($publisherStub->reveal());
        $testedObject->publish();
    }
}

附带说明:如果您的 Foo 类 需要 Publisher 进行工作,您应该通过构造函数而不是 setter 方法设置它。仅对 可选 依赖项使用 setter 方法


更新

所以根据我在实际代码中建议的 cmets,您正在使用 new 像这样创建 Publisher 类的对象

public function publishMessage() {
    $message   = $this->generateMessage();
    $publisher = new Publisher;
    $publisher->publish($message);
}

或者你可能直接使用Redis::publish静态方法

public function publishMessage() {
    $message = $this->generateMessage();
    Redis::publish($message);
}

嗯,这被称为 耦合 类,被认为是一种不好的做法,因为在 SOLID 中违反了 D。 尽管如此,在这种情况下还是有一个 workaround 用于模拟/存根依赖项,同样使用匿名类。

假设尚未加载依赖类,您可以执行以下操作:

$class = new class() {
    function publish(string $message) {
        assert($message === 'expected');
    }
};
class_alias(get_class($class), 'Redis');

如果你在多个测试中重复这个技巧,你会收到警告:

PHP 警告:不能声明类 Redis,因为名称是 已经在使用中

要克服它,您需要使用 --process-isolation 运行测试

我认为我们永远不应该这样做(这是一个肮脏的黑客)并使用DI,但有时我们会处理遗留问题

【讨论】:

  • 谢谢,我会试试这个。我想我模拟该方法的想法是你用“testFooWithAnonymousClass”展示的。虽然我对此还是不太满意。旁注:这就是我不满意的地方。我添加 setter 只是为了测试能够传递一个模拟的发布者对象。对于实际代码,它是不需要的,这就是为什么我没有将它添加到构造函数中。
  • 你的意思是你在实际代码中调用Foo::publish()方法中的Redis::publish($message);?我认为如果你发布你的实际代码会更好,这样就会清楚想要什么
  • 这是实际代码,只是删除和重命名的东西。我的意思是 Foo::publish 本身。它不需要调用 $this->getPublisher,我添加了这个,所以我可以传递一个模拟的 Publisher 类。
  • 谢谢。我不能做 D,因为“Foo”的调用方式不允许访问构造函数传递任何东西。我想重写这一切。因此我正在编写测试。
猜你喜欢
  • 2017-04-07
  • 1970-01-01
  • 1970-01-01
  • 2013-03-23
  • 2014-08-13
  • 2012-08-04
  • 1970-01-01
  • 2022-06-19
  • 1970-01-01
相关资源
最近更新 更多