• 由于被测系统可能依赖于其他组件,而这些组件在测试环境中不可用/有不良副作用等等,导致测试很困难。而使用测试替身不需要完整的行为,只需要提供同样的API即可
  • PHPUnit提供了 createMock()getMockBuilder() 方法,用来自动生成对象。 createMock() 方法使用的是默认值:不执行原始类的构造方法、克隆方法,且不对传递给测试替身的方法的参数进行克隆
  • 注意: finalprivatestatic 方法是无法制作Stub或者Mock的,PHPUnit将会维持其原始行为。不过static方法有所不同,会被替换为一个抛出 \PHPUnit\Framework\MockObject\BadMethodCallException 异常的方法
  • 默认情况下(在不设置返回值的时候),原class的所有方法会被替换为只返回null的伪实现

注: 在浏览英文文档的时候,发现还提供了 createStub() 方法,以下 Stubs 中的 createMock() 方法都可以使用 createStub() 方法进行替换。还有部分新的特性

1. Stubs桩件

概述

上桩概念 :将被依赖的、不好测的对象,替换为返回指定返回值的测试替身。即用桩件stub替换掉被测系统所依赖的组件。
流程:

  1. 创建桩件
  2. 配置桩件
  3. 调用

例子:

  1. <?php
  2. use PHPUnit\Framework\TestCase;
  3. //被依赖的,需要上桩的类
  4. class SomeClass
  5. {
  6. public function doSomething()
  7. {
  8. // 随便做点什么。
  9. }
  10. }
  11. ?>
  12. <?php
  13. use PHPUnit\Framework\TestCase;
  14. class StubTest extends TestCase
  15. {
  16. public function testStub()
  17. {
  18. // 为 SomeClass 类创建桩件。
  19. // 该方法将自动生成一个新的类来实现想要的行为
  20. $stub = $this->createMock(SomeClass::class);
  21. // 配置桩件。
  22. $stub->method('doSomething')
  23. ->willReturn('foo'); //这是种简单语法,等效于->will($this->returnValue())
  24. // 现在调用 $stub->doSomething() 将返回 'foo'。
  25. $this->assertEquals('foo', $stub->doSomething());
  26. }
  27. }
  28. ?>

注意: 可以看到,配置桩件的时候,调用了method方法。所以若原始类中包含了method方法,就无法正常运行。不过可以通过 $stub->expects($this->any())->method('doSomething')->willReturn('foo');来使用

$this->creatMock() 的效果可以通过 $this->createBuilder() 来实现:

  1. <?php
  2. //$stub = $this->createMock()的等效操作
  3. $stub = $this->getMockBuilder($originalClassName)
  4. ->disableOriginalConstructor()
  5. ->disableOriginalClone()
  6. ->disableArgumentCloning()
  7. ->disallowMockingUnknownTypes()
  8. ->getMock();
  9. ?>
  1. <?php
  2. //will()方法的各类操作
  3. //返回自身引用
  4. $stub->method('funcName')->will($this->returnSelf());
  5. $this->assertSame($stub, $stub->doSomething());//Same
  6. //设置返回值
  7. $stub->method('doSomething')->will($this->returnValue('foo'));
  8. $this->assertEquals('foo', $stub->doSomething());
  9. //返回传入的参数
  10. $stub->method('funcName')->will($this->returnArgument(0));
  11. $this->assertEquals('foo', $stub->doSomething('foo'));
  12. //根据映射返回内容
  13. $map = [ //两组映射
  14. ['a', 'b', 'c', 'd'],
  15. ['e', 'f']
  16. ];
  17. $stub->method('doSomething')->will($this->returnValueMap($map));
  18. $this->assertEquals('d', $stub->doSomething('a', 'b', 'c'));
  19. //返回回调函数的结果
  20. $stub->method('doSomething')->will($this->returnCallback('str_rot13'));
  21. $this->assertEquals('fbzrguvat', $stub->doSomething('something'));
  22. //连续的返回值
  23. $stub->method('doSomething')->will($this->onConsecutiveCalls(1,2));
  24. $this->assertEquals(1, $stub->doSomething());
  25. $this->assertEquals(2, $stub->doSomething());
  26. $this->assertEquals(null, $stub->doSomething());
  27. //抛出异常
  28. $stub->method('doSomething')->will($this->throwException(new Exception));

关于指定了返回类型的方法

如下例子

  1. <?php declare(strict_types=1);
  2. class C
  3. {
  4. public function m(): D
  5. {
  6. //do something
  7. }
  8. }

对于指定了返回类型的方法,其默认返回值为该类型的空值(如float中的0.0)。如果 D 是一个矢量类型(如对象),那么将会自动创建一个 D 的测试替身作为返回值,这个返回值形如:
image.png

官方文档对于使用桩件的建议

系统中被广泛使用的资源是通过单个外观(facade)来访问的,因此很容易就能用桩件替换掉资源。例如,将散落在代码各处的对数据库的直接调用替换为单个 Database 对象,这个对象实现了 IDatabase 接口。接下来,就可以创建实现了 IDatabase 的桩件并在测试中使用之。甚至可以创建一个选项来控制是用桩件还是用真实数据库来运行测试,这样测试就既能在开发过程中用作本地测试,又能在实际数据库环境中进行集成测试。

需要上桩的功能往往集中在同一个对象中,这就改善了内聚度。将功能通过单一且一致的接口呈现出来,就降低了这部分与系统其他部分之间的耦合度。

2. 仿件对象Mock Object

模仿mocking: 将对象替换为能验证预期行为(如断言某个方法必会调用)的测试替身的实践方法

先看个例子,首先是被测系统的定义

  1. <?php
  2. use PHPUnit\Framework\TestCase;
  3. //Subject和Obeserver构成了被测系统SUT
  4. class Subject
  5. {
  6. protected $observers = [];
  7. protected $name;
  8. public function __construct($name)
  9. {
  10. $this->name = $name;
  11. }
  12. public function getName()
  13. {
  14. return $this->name;
  15. }
  16. public function attach(Observer $observer)
  17. {
  18. $this->observers[] = $observer;
  19. }
  20. public function doSomething()
  21. {
  22. // 做点什么
  23. // ...
  24. // 通知观察者发生了些什么
  25. $this->notify('something');
  26. }
  27. public function doSomethingBad()
  28. {
  29. foreach ($this->observers as $observer) {
  30. $observer->reportError(42, 'Something bad happened', $this);
  31. }
  32. }
  33. protected function notify($argument)
  34. {
  35. foreach ($this->observers as $observer) {
  36. $observer->update($argument);
  37. }
  38. }
  39. // 其他方法。
  40. }
  41. class Observer
  42. {
  43. public function update($argument)
  44. {
  45. // 做点什么。
  46. }
  47. public function reportError($errorCode, $errorMessage, Subject $subject)
  48. {
  49. // 做点什么。
  50. }
  51. // 其他方法。
  52. }
  53. ?>

由于仿件关注的是,检验某个方法的调用,以及调用时的交互,所以按照下列方式对仿件进行配置

  1. <?php
  2. use PHPUnit\Framework\TestCase;
  3. class SubjectTest extends TestCase
  4. {
  5. public function testObserversAreUpdated()
  6. {
  7. // 创建仿件
  8. // 为 Observer 类建立仿件对象,只模仿 update() 方法。
  9. $observer = $this->getMockBuilder(Observer::class)
  10. ->setMethods(['update'])
  11. ->getMock();
  12. // 配置仿件,使用expects()和with()来指明交互
  13. // 建立预期状况:update() 方法将会被调用一次,
  14. // 并且将以字符串 'something' 为参数。
  15. $observer->expects($this->once())
  16. ->method('update')
  17. ->with($this->equalTo('SOMETHING')); //这里即为断言的期望值
  18. // 创建 Subject 对象,并将模仿的 Observer 对象连接其上。
  19. $subject = new Subject('My subject');
  20. $subject->attach($observer);
  21. // 在 $subject 对象上调用 doSomething() 方法,
  22. // 预期将以字符串 'something' 为参数调用
  23. // Observer 仿件对象的 update() 方法。
  24. $subject->doSomething();
  25. }
  26. }
  27. ?>
  28. [@hbhly_75_237 TestCode]$ phpunit ClassTest.php
  29. PHPUnit 9.4.2 by Sebastian Bergmann and contributors.
  30. F 1 / 1 (100%)
  31. Time: 00:00.003, Memory: 18.00 MB
  32. There was 1 failure:
  33. 1) ClassTest::testObserversAreUpdated
  34. Expectation failed for method name is "update" when invoked 1 time(s)
  35. Parameter 0 for invocation Observer::update('something') does not match expected value.
  36. Failed asserting that two strings are equal.
  37. --- Expected
  38. +++ Actual
  39. @@ @@
  40. -'SOMETHING'
  41. +'something'
  42. /search/xuyixiang/TestCode/ClassTest.php:73
  43. /search/xuyixiang/TestCode/ClassTest.php:60
  44. /search/xuyixiang/TestCode/ClassTest.php:26
  45. FAILURES!
  46. Tests: 1, Assertions: 0, Failures: 1.
  • with() 方法携带的参数数量,对应于被模仿的方法的参数数量,可以对参数进行匹配以及各种约束,如大于小于、字符串包含等。
  • withConsecutive() 方法可以接受数组作为参数,其数量对应于 $this->exactly(num) 中指定的次数
  • callback() 可以进行更加复杂的参数校验

    1. <?php
    2. $observer->expects($this->once())
    3. ->method('reportError')
    4. ->with($this->greaterThan(0),
    5. $this->stringContains('Something'),
    6. $this->callback(function($subject){
    7. return is_callable([$subject, 'getName']) &&
    8. $subject->getName() == 'My subject';
    9. }));
  • 用于指定调用次数的匹配器 | 匹配器 | 含义 | | —- | —- | | PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount any() | 返回一个匹配器,当被评定的方法执行0次或更多次(即任意次数)时匹配成功。 | | PHPUnit_Framework_MockObject_Matcher_InvokedCount never() | 返回一个匹配器,当被评定的方法从未执行时匹配成功。 | | PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce atLeastOnce() | 返回一个匹配器,当被评定的方法执行至少一次时匹配成功。 | | PHPUnit_Framework_MockObject_Matcher_InvokedCount once() | 返回一个匹配器,当被评定的方法执行恰好一次时匹配成功。 | | PHPUnit_Framework_MockObject_Matcher_InvokedCount exactly(int $count) | 返回一个匹配器,当被评定的方法执行恰好 $count 次时匹配成功。 | | PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex at(int $index) | 返回一个匹配器,当被评定的方法是第 $index 个执行的方法时匹配成功。 |

3. Prophecy(预言)——对象模仿框架

最新的官方文档已经将这个删去了

PHPUnit对这个测试替身框架提供了内建支持

4. 对特质(Trait)与抽象类进行模仿

getMockForTrait()
getMockForAbstractClass()

5. 对Web服务进行上桩或模仿

可以与web服务不进行实际交互的情况下对其进行模仿

方法: getMockFromWsdl()
该方法所返回的桩件或仿件是基于以WSDL描述的web服务,而getMock()返回的桩件/仿件是基于PHP类或者接口的

6. 对文件系统进行模仿

最新的官方文档已经删去该部分

官方描述:

vfsStream是对虚拟文件系统的流包覆器(stream wrapper),可以用于模拟真实文件系统

优势:

  • 文件系统和外部资源一样,可能间歇性出现问题,解决对其的依赖可以使得测试更加可靠
  • setUp()tearDown() 中,想要使这个目录/文件在测试前/后均不存在。而且若在tearDown()之前终止了,则目录将会遗留下来

安装:

  • 使用composer的话,只需要添加如下依赖即可
    1. {
    2. "require-dev": {
    3. "phpunit/phpunit": "~4.6",
    4. "mikey179/vfsStream": "~1"
    5. }
    6. }

关于测试替身test double

Gerard Meszaros《xUnit测试模式》关于测试替身——用于替换真实对象的模拟对象——的论述

  • Dummy
    • 用于传递给调用者,但是永远不会被真实使用的对象。通常只是用于填满参数列表
  • Fake
    • Fake对象常常和类的实现一起发生作用,但只是为了让其他程序能正常运行。譬如内存数据库(没深入了解)
  • Stubs
    • 用于在测试中提供封装好的响应。Stubs通常就是对一个真实对象的封装
  • Mocks
    • 针对设定好的调用方法与需要响应的参数封装出合适的对象