我有这门课,我正在测试那些在更深处使用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的模拟库存在,但不支持发布。
答案 0 :(得分:2)
一些选项被描述为测试类的方法
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。 尽管如此,在这种情况下,还有一个workaround用于模拟/存根依赖,同样使用匿名类。
假设尚未加载依赖类,您可以执行以下操作:
$class = new class() {
function publish(string $message) {
assert($message === 'expected');
}
};
class_alias(get_class($class), 'Redis');
如果你在多次测试中重复这个技巧,你会收到警告:
PHP警告:无法声明类Redis,因为名称是 已经在使用
要克服它,您需要使用--process-isolation
我认为我们永远不应该这样做(这是一个肮脏的黑客)并使用DI,但有时我们会处理遗产