测试替身
在测试的时候,我们会经常遇到引用了外部依赖的对象,比如:发送邮件、Log记录、文件系统操作等等,这些外部依赖我们很难控制,这时,就可以通过测试替身来模拟这些外部依赖行为,从而让测试进行下去。
PHPUnit 提供了生成测试替身对象的方法:
createMock($type)
getMockBuilder($type)
前者返回的对象 = 后者使用默认值生成的对象
注意:final
、private
和 static
方法无法对其进行上桩(stub
)或模仿(mock
)。PHPUnit 的测试替身功能将会忽略它们,并维持它们的原始行为。
桩件
将对象替换为 返回配置好的返回值的测试替身的实践方法称为上桩(stubbing)。
桩件用法示例:
假设有这样一个邮件功能
class Email
{
public function sendEmail()
{
echo 'send email success';
return true;
}
}
用桩件来进行模拟:
- 返回简单指定值
$stub = $this->createMock(Email::class); //配置桩件:指定返回值 $stub->method('sendEmail')//指定替身方法,不能指定不存在的方法 ->willReturn(true); $this->assertEquals(true, $stub->sendEmail());
- 通过变量返回更复杂的数据(不用变量之间写也行)
$stub = $this->createMock(Email::class); $res = [ 'flag' => 'success', 'code' => 200 ]; $stub->method('sendEmail')->willReturn($res); $this->assertEquals($res, $stub->sendEmail());
- 返回指定索引的参数值
$stub = $this->createMock(Email::class); //指定下标的参数值作为返回值 $stub->method('sendEmail')->will($this->returnArgument(0)); $this->assertEquals('345@qq.com', $stub->sendEmail('345@qq.com'));
- 返回桩件的自身引用
$stub = $this->createMock(Email::class); $stub->method('sendEmail')->will(TestCase::returnSelf()); $this->assertSame($stub, $stub->sendEmail());
- 根据参数和值的映射进行返回
stub = $this->createMock(Email::class); //最后一个值为返回值,前面都是参数 $map = [ ['zs@qq.com', true], ['ls@qq.com', false], ]; $stub->method('sendEmail') ->will($this->returnValueMap($map)); $this->assertTrue($stub->sendEmail('zs@qq.com')); $this->assertTrue($stub->sendEmail('ls@qq.com'));
- 由回调生成返回值
$stub = $this->createMock(Email::class); $stub->method('sendEmail') ->will($this->returnCallback('strtoupper')); //返回对参数使用回调函数后的结果 $this->assertEquals('HELLO', $stub->sendEmail('hello'));
- 按指定顺序迭代返回
$stub = $this->createMock(Email::class); $stub->method('sendEmail') ->will($this->onConsecutiveCalls('hi', 'world', '!')); $this->assertEquals('hi', $stub->sendEmail()); $this->assertEquals('world', $stub->sendEmail()); $this->assertEquals('!', $stub->sendEmail());
- 指定抛出异常
$stub = $this->createMock(Email::class); $stub->method('sendEmail') ->will($this->throwException(new \InvalidArgumentException())); //将抛出异常 $stub->sendEmail();
仿件
将对象替换为能验证预期行为(例如断言某个方法必会被调用)的测试替身的实践方法称为模仿(mocking)。
可以简单理解为:桩件 + 预期管理
相关 API
仿件生成器 getMockBuilder($type)
提供的方法:
setMethods(array $methods)
指定哪些方法将被替换为可配置的测试替身。其他方法的行为不会有所改变。如果调用 setMethods(null),那么没有方法会被替换。setConstructorArgs(array $args)
用于向原版类的构造函数(默认情况下不会被替换为伪实现)提供参数数组setMockClassName($name)
用于指定生成的测试替身类的类名disableOriginalConstructor()
可用于禁用对原版类的构造方法的调用disableOriginalClone()
可用于禁用对原版类的克隆方法的调用disableAutoload()
用于在测试替身类的生成期间禁用 __autoload()
通过以下方法实现预期管理:
expects()
any()
方法执行0次或更多次(即任意次数)时匹配成功never()
方法从未执行时匹配成功atLeastOnce()
方法执行至少一次时匹配成功once()
方法执行恰好一次时匹配成功exactly(int $count)
方法执行恰好 $count 次时匹配成功at(int $index)
方法是第 $index 个执行的方法时匹配成功 $index 参数指的是对给定仿件对象的所有方法的调用的索引,从零开始。使用这个匹配器要谨慎
with()
对参数的预期管理equalTo(string $str)
预期参数将等于指定值greaterThan($value)
预期参数值将大于指定数值greaterThanOrEqual($value)
预期参数值将大于等于指定数值stringContains(string $str)
预期参数将包含字指定符串anything()
预期参数可以为任意数据callback()
对参数进行回调验证,回调方法应该返回一个bool
,其参数为待校验的参数identicalTo()
校验参数是否和预期数据一致,用于对象比较isTrue()
isFalse()
isNull()
isJson()
withConsecutive()
可以接受任意多个数组作为参数,每个数组都都是对被仿方法的相应参数的一组约束,其内容同with()
中的预期
示例:对于基于SPL接口实现的观察者模式,可以如下测试 update
方法有没有被正确执行:
public function testRegister()
{
//测试:当被观察者通知观察者时,观察者是否有执行 update 方法
//生成一个仿件
$mockObserver = $this->getMockBuilder(Observer::class)->setMethods(['update'])->getMock();
$subjectObj = new Subject();
//预期管理:update方法只会被执行一次,执行update方法时候的参数是:$subjectObj对象
$mockObserver->expects($this->once())
->method('update')
->with($this->identicalTo($subjectObj));
$subjectObj->attach($mockObserver);
$subjectObj->notify();
}
示例2:
//生成一个仿件
$mock = $this->getMockBuilder(Observer::class)->setMethods(['doSomething'])->getMock();
//预期管理:doSomething 方法会被执行2次
//第一次:参数1:必须是字符串:“Hi”;参数2:值必须大于等于2;
//第二次:参数1:可以为任意数据;参数2:必须是布尔true
$mock->expects($this->exactly(3))->method('doSomething')->withConsecutive(
[$this->equalTo('Hi'), $this->greaterThanOrEqual(2)],
[$this->anything(),$this->isTrue()],
[$this->stringContains('test'), $this->callback(function (int $arg) {
return $arg % 2 == 0;
})]
);
$mock->doSomething('Hi', 3);
$mock->doSomething('Any', true);
$mock->doSomething('phpunit-test', 5);//预期失败,不是偶数,回调会返回false
模仿 Trait 和 抽象类
-
getMockForTrait()
返回一个使用了特定 trait 的仿件对象trait AbstractTrait { public function concreteMethod() { return $this->abstractMethod(); } public abstract function abstractMethod(); } class TraitClassTest extends TestCase { public function testConcreteMethod() { $mock = $this->getMockForTrait(AbstractTrait::class); $mock->expects($this->any()) ->method('abstractMethod') ->will($this->returnValue(true)); $this->assertTrue($mock->concreteMethod()); } }
-
getMockForAbstractClass()
返回一个抽象类的仿件对象use PHPUnit\Framework\TestCase; abstract class AbstractClass { public function concreteMethod() { return $this->abstractMethod(); } public abstract function abstractMethod(); } class AbstractClassTest extends TestCase { public function testConcreteMethod() { $stub = $this->getMockForAbstractClass(AbstractClass::class); $stub->expects($this->any()) ->method('abstractMethod') ->will($this->returnValue(true)); $this->assertTrue($stub->concreteMethod()); } }
对 Web 服务(Web Services)进行上桩或模仿
对文件系统进行模仿
vfsStream 是对虚拟文件系统 的 stream wrapper,可以用于模仿真实文件系统
可以使用:mikey179/vfsStream
依赖包。
Prophecy
Prophecy 是一个对象模仿框架,PHPUnit 对用 Prophecy 建立测试替身提供了内建支持
示例:
public function testObserversAreUpdated()
{
$subject = new Subject('My subject');
// 为 Observer 类建立预言(prophecy)。
$observer = $this->prophesize(Observer::class);
// 建立预期状况:update() 方法将会被调用一次,
// 并且将以字符串 'something' 为参数。
$observer->update('something')->shouldBeCalled();
// 揭示预言,并将仿件对象链接到主体上。
$subject->attach($observer->reveal());
// 在 $subject 对象上调用 doSomething() 方法,
// 预期将以字符串 'something' 为参数调用
// Observer 仿件对象的 update() 方法。
$subject->doSomething();
}
public function testDemo()
{
//建立预言
$prop = $this->prophesize(Observer::class);
//语言内容:doSomething 会被调用至少一次,且参数为:hi 2019
$prop->doSomething('hi', 2019)->shouldBeCalled();
//执行方法一次
$prop->reveal()->doSomething('hi', 2019);
//语言失败:
$prop->reveal()->doSomething('hi', 2018);
}