如何模拟一个类并覆盖其中的方法

5

我正在测试一个使用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有一个模拟库,但不支持发布。

1
我没有得到答案,但你忘记给 $message 赋值了:class Publisher { public $message; function publish($message) { $this->message = $message; } } - José Carlos PHP
谢谢您的关注。 - steros
1个回答

4

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

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方法用于可选依赖项。

更新

从评论中我建议在实际代码中,您使用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);
}

这被称为耦合类,被认为是一种不好的做法,因为违反了D中的SOLID原则。 尽管如此,在这种情况下,仍然有一种解决方法可以用于模拟/存根依赖关系,即使用匿名类。 假设依赖类还没有加载,您可以像这样操作:
$class = new class() {
    function publish(string $message) {
        assert($message === 'expected');
    }
};
class_alias(get_class($class), 'Redis');

如果您在多个测试中重复使用此技巧,您将收到警告:

PHP Warning: Cannot declare class Redis, because the name is already in use

为了克服这个问题,您需要使用--process-isolation运行测试。
我认为我们不应该这样做(这是一种肮脏的技巧),而是使用 DI,但有时我们需要处理遗留代码。

谢谢,我会尝试这个方法。我想我的模拟方法的想法就是你展示的“testFooWithAnonymousClass”。虽然我仍然不太满意。对于你的侧记:这就是我不满意的地方。我只添加了setter来进行测试,以便能够传递模拟的发布者对象。对于实际代码,它是不需要的,这就是为什么我没有将其添加到构造函数中的原因。 - steros
你的意思是在实际代码中,在 Foo::publish() 方法中调用了 Redis::publish($message); 吗?我认为最好你把你的实际代码贴出来,这样就能清楚地看到想要的是什么。 - Nikita U.
这是实际的代码,只是删除和重命名了一些东西。我的意思是Foo::publish本身。它不需要调用$this->getPublisher,我添加了这个以便可以传递一个模拟的Publisher类。 - steros
谢谢。我无法执行D,因为在上面的“Foo”被称为一种不允许访问构造函数传递任何内容的方式。我想重写所有这些。因此,我正在编写测试。 - steros
在你的最后一句话中,你推荐使用依赖注入 - 在 PHP 中通常使用 resolve() 函数,对吗?但是问题和答案都没有涉及到涉及到使用 resolve() 的模拟示例。您能否提供一个设置的示例链接? - Sarah Messer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接