PHPUnit中的Mock和Stub的区别

44

我知道存根(stubs)验证状态,而模拟对象(mocks)则验证行为。

PHPUnit 中如何创建一个模拟对象以验证方法的行为?PHPUnit 没有验证方法(verify()),而且我不知道如何在 PHPUnit 中创建一个模拟对象。

文档中很好地解释了如何创建存根。

// Create a stub for the SomeClass class.
$stub = $this->createMock(SomeClass::class);

// Configure the stub.
$stub
    ->method('doSomething')
    ->willReturn('foo');

// Calling $stub->doSomething() will now return 'foo'.
$this->assertEquals('foo', $stub->doSomething());

但在这种情况下,我正在验证状态,说明需要返回一个答案。

如何创建一个模拟对象并验证行为的示例?


1
文档说明了模拟对象的使用,包括模拟测试中的各种情况。 - xmike
3个回答

98

PHPUnit曾经支持两种创建测试替身的方式,除了传统的PHPUnit mocking框架外,我们还可以选择使用Prophecy。

PHPUnit 9中删除了对Prophecy的支持,但是可以通过安装phpspec/prophecy-phpunit来重新添加支持。

PHPUnit Mocking Framework

createMock方法用于创建三种常见的测试替身。通过配置对象,可以将其配置为dummy、stub或mock。

您还可以使用mock builder创建测试stubs(getMockBuilder返回mock builder)。这只是另一种做同样事情的方法,它允许您使用流畅的接口调整一些额外的mock选项(有关更多信息,请参见文档)。

Dummy

Dummy参数被传递,但从未实际调用,或者如果被调用,则会响应默认答案(大多数情况下为 null)。它主要存在以满足参数列表。

$dummy = $this->createMock(SomeClass::class);

// SUT - System Under Test
$sut->action($dummy);

桩(Stub)

桩通常用于查询类的方法 - 即返回结果的方法,但其是否实际调用并不重要。

$stub = $this->createMock(SomeClass::class);
$stub->method('getSomething')
    ->willReturn('foo');

$sut->action($stub);

Mock

Mock用于命令式的方法 - 重要的是它们被调用,而我们并不太关心它们的返回值(命令式方法通常不返回任何值)。

$mock = $this->createMock(SomeClass::class);
$mock->expects($this->once())
    ->method('doSomething')
    ->with('bar');

$sut->action($mock);

在您的测试方法执行完毕后,预期结果将自动进行验证。在上面的示例中,如果在SomeClass上未调用方法doSomething,或者使用与配置的参数不同的参数进行了调用,则测试将失败。

Spy

不支持。

Prophecy

现在PHPUnit已经原生支持Prophecy,因此您可以将其作为传统Mocking框架的替代方案使用。同样,对象的配置方式决定了它是哪一种类型的测试Double。

Dummy

$dummy = $this->prophesize(SomeClass::class);

$sut->action($dummy->reveal());

$stub = $this->prophesize(SomeClass::class);
$stub->getSomething()->willReturn('foo');

$sut->action($stub->reveal());

模拟

$mock = $this->prophesize(SomeClass::class);
$mock->doSomething('bar')->shouldBeCalled();

$sut->action($mock->reveal());

间谍

$spy = $this->prophesize(SomeClass::class);

// execute the action on system under test
$sut->action($spy->reveal());

// verify expectations after 
$spy->doSomething('bar')->shouldHaveBeenCalled();

你确实需要更新答案。谢谢! :) - Jakub Zalas
createMock()和getMockBuilder()有什么区别,在文档中制作Mocks使用getMockBuilder()方法。这有关系吗?谢谢! - RodriKing
那是一个不同的问题 ;) 它在文档中有说明:"createMock($type) 方法立即返回指定类型(接口或类)的测试替身对象。创建此测试替身使用最佳实践默认值(原始类的 __construct() 和 __clone() 方法不会执行),传递给测试替身方法的参数将不会被克隆。如果这些默认值不符合您的需求,则可以使用 getMockBuilder($type) 方法使用流畅的界面自定义测试替身生成。" - Jakub Zalas
你绝对可以使用PHPUnit模拟对象来定义间谍:$spy = $this->createMock(Foo::class); $spy->expect($this->once('bar'))->with($this->identicalTo('baz'))->willReturn('qux');。如果 $spy 作为协作者被使用,但从未使用相应的参数进行调用,则测试将失败。 - localheinz
@localheinz 对于 Spy 的期望应该在调用 SUT 之后定义。我不认为你的例子这样会起作用。还有另外一种解决方法,但它仍然是一个解决方法。Phpunit 默认情况下不支持 Spies(除了使用 Prophecy)。请参见 https://github.com/sebastianbergmann/phpunit-mock-objects/issues/333. - Jakub Zalas
显示剩余4条评论

6

仿件(Dummies)

首先看一下仿件。如果你让我记住汽车钥匙放在哪里,我会变成什么样子...同时,如果你在 phpspec 中添加了一个带有类型提示的参数来获取测试双倍对象,却没有对其进行任何操作,那么你得到的就是一个仿件对象。因此,如果我们获取一个测试双倍对象并对其方法不添加任何行为和断言,则称其为“仿件对象”。

哦,还有,在它们的文档中,你会看到像 $prophecy->reveal() 这样的东西。这是一个细节,我们不需要担心,因为 phpspec 会为我们处理。好评!

存根(Stubs)

只要您开始控制甚至一个方法的返回值...砰!这个对象突然就被称为一个存根。从文档中可以看出: "一个存根是一个对象的双倍" - 所有这些东西都被称为测试双倍或者对象双倍,当放置在特定环境中时,表现出特定的行为方式。这是说,只要我们添加一个 willReturn(),它就成为了一个存根。

实际上,大部分文档都花在了谈论存根和控制其行为的不同方法上,包括我们之前看到的参数通配符。

模拟对象(Mocks)

如果您继续往下阅读,下一件事情就是 "模拟对象"。当您调用 shouldBeCalled() 时,一个对象就变成了模拟对象。因此,如果您想添加一个断言,表明某个方法被调用了特定次数,并且您想在实际代码之前放置该断言——使用 shouldBeCalledTimes() 或 shouldBeCalled()——恭喜您!你的对象现在是一个模拟对象。

间谍对象(Spies)

最后,在底部,我们有间谍对象。间谍对象与模拟对象完全相同,只是当您在代码后面添加期望时,比如 shouldHaveBeenCalledTimes()。

https://symfonycasts.com/screencast/phpspec/doubles-dummies-mocks-spies


所以某些东西可能既是存根(stub),又是模拟(mock)?例如 $double->method('x')->shouldBeCalled()->willReturn(...) - artfulrobot
是的,如果您对调用对象的次数进行断言,它就是一个模拟。 - Mhmd

0

简而言之,我们可以说:

您可以使用模拟对象“作为观察点,用于验证在执行SUT时的间接输出。通常,模拟对象还包括测试存根的功能,如果它尚未未通过测试,则必须向SUT返回值,但重点是验证间接输出。因此,模拟对象不仅仅是测试存根加断言;它以根本上不同的方式使用


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