2. 编写 PHPUnit 测试

    1. 针对类 的测试写在类 ClassTest 中。

    2. ClassTest(通常)继承自 PHPUnit\Framework\TestCase

    3. 测试都是命名为 test* 的公用方法。

      也可以在方法的文档注释块(docblock)中使用 @test 标注将其标记为测试方法。

    4. 在测试方法内,类似于 assertSame()(参见)这样的断言方法用来对实际值与预期值的匹配做出断言。

    示例 2.1 用 PHPUnit 测试数组操作

    Martin Fowler

    Adrian Kuhn et. al.

    单元测试主要是作为一种良好实践来编写的,它能帮助开发人员识别并修复 bug、重构代码,还可以看作被测软件单元的文档。要实现这些好处,理想的单元测试应当覆盖程序中所有可能的路径。一个单元测试通常覆盖一个函数或方法中的一个特定路径。但是,测试方法不一定是封装良好的独立实体。测试方法之间经常有隐含的依赖关系暗藏在测试的实现方案中。

    PHPUnit支持对测试方法之间的显式依赖关系进行声明。这种依赖关系并不是定义在测试方法的执行顺序中,而是允许生产者(producer)返回一个测试基境(fixture)的实例,并将此实例传递给依赖于它的消费者(consumer)们。

    • 生产者(producer),是能生成被测单元并将其作为返回值的测试方法。
    • 消费者(consumer),是依赖于一个或多个生产者及其返回值的测试方法。

    示例 2.2 展示了如何用 @depends 标注来表示测试方法之间的依赖关系。

    示例 2.2 用 @depends 标注来表示依赖关系

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class StackTest extends TestCase
    4. {
    5. public function testEmpty(): array
    6. {
    7. $stack = [];
    8. $this->assertEmpty($stack);
    9. return $stack;
    10. }
    11. /**
    12. * @depends testEmpty
    13. */
    14. public function testPush(array $stack): array
    15. {
    16. array_push($stack, 'foo');
    17. $this->assertSame('foo', $stack[count($stack)-1]);
    18. $this->assertNotEmpty($stack);
    19. return $stack;
    20. }
    21. /**
    22. * @depends testPush
    23. */
    24. public function testPop(array $stack): void
    25. {
    26. $this->assertSame('foo', array_pop($stack));
    27. $this->assertEmpty($stack);
    28. }
    29. }

    在上例中,第一个测试testEmpty() 创建了一个新数组,并断言其为空。随后,此测试将此基境作为结果返回。第二个测试 testPush() 依赖于 testEmpty(),并将所依赖的测试之结果作为参数传入。最后,testPop() 依赖于 testPush()

    默认情况下,生产者所产生的返回值将“原样”传递给相应的消费者。这意味着,如果生产者返回的是一个对象,那么传递给消费者的将是指向此对象的引用。但同样也可以(a)通过 @depends clone 来传递指向(深)拷贝对象的引用,或(b)通过 @depends shallowClone 来传递指向(正常浅)克隆对象(基于 PHP 关键字 clone)的引用。

    为了定位缺陷,我们希望把注意力集中于相关的失败测试上。这就是为什么当某个测试所依赖的测试失败时,PHPUnit 会跳过这个测试。利用测试之间的依赖关系可以改进缺陷定位,如 所示。

    示例 2.3 利用测试之间的依赖关系

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class DependencyFailureTest extends TestCase
    4. {
    5. public function testOne(): void
    6. {
    7. $this->assertTrue(false);
    8. }
    9. /**
    10. * @depends testOne
    11. */
    12. public function testTwo(): void
    13. {
    14. }
    15. }
    1. $ phpunit --verbose DependencyFailureTest
    2. PHPUnit latest.0 by Sebastian Bergmann and contributors.
    3. FS
    4. Time: 0 seconds, Memory: 5.00Mb
    5. There was 1 failure:
    6. 1) DependencyFailureTest::testOne
    7. Failed asserting that false is true.
    8. /home/sb/DependencyFailureTest.php:6
    9. There was 1 skipped test:
    10. 1) DependencyFailureTest::testTwo
    11. This test depends on "DependencyFailureTest::testOne" to pass.
    12. FAILURES!
    13. Tests: 1, Assertions: 1, Failures: 1, Skipped: 1.

    测试可以使用多个 @depends 标注。PHPUnit 不会更改测试的运行顺序,因此你需要自行保证某个测试所依赖的所有测试均出现于这个测试之前。

    拥有多个 @depends 标注的测试,其第一个参数是第一个生产者提供的基境,第二个参数是第二个生产者提供的基境,以此类推。参见示例 2.4

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class MultipleDependenciesTest extends TestCase
    4. {
    5. public function testProducerFirst(): string
    6. {
    7. $this->assertTrue(true);
    8. return 'first';
    9. }
    10. public function testProducerSecond(): string
    11. {
    12. $this->assertTrue(true);
    13. return 'second';
    14. }
    15. /**
    16. * @depends testProducerFirst
    17. * @depends testProducerSecond
    18. */
    19. public function testConsumer(string $a, string $b): void
    20. {
    21. $this->assertSame('first', $a);
    22. $this->assertSame('second', $b);
    23. }
    24. }
    1. $ phpunit --verbose MultipleDependenciesTest
    2. PHPUnit latest.0 by Sebastian Bergmann and contributors.
    3. ...
    4. Time: 0 seconds, Memory: 3.25Mb
    5. OK (3 tests, 4 assertions)

    数据供给器

    测试方法可以接受任意参数。这些参数由一个或多个数据供给器方法(在 中,是 additionProvider() 方法)提供。用 @dataProvider 标注来指定要使用的数据供给器方法。

    数据供给器方法必须声明为 public,其返回值要么是一个数组,其每个元素也是数组;要么是一个实现了 Iterator 接口的对象,在对它进行迭代时每步产生一个数组。每个数组都是测试数据集的一部分,将以它的内容作为参数来调用测试方法。

    示例 2.5 使用返回数组的数组的数据供给器

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class DataTest extends TestCase
    4. {
    5. /**
    6. * @dataProvider additionProvider
    7. */
    8. public function testAdd(int $a, int $b, int $expected): void
    9. {
    10. $this->assertSame($expected, $a + $b);
    11. }
    12. public function additionProvider(): array
    13. {
    14. return [
    15. [0, 0, 0],
    16. [0, 1, 1],
    17. [1, 0, 1],
    18. [1, 1, 3]
    19. ];
    20. }
    21. }
    1. $ phpunit DataTest
    2. PHPUnit latest.0 by Sebastian Bergmann and contributors.
    3. ...F
    4. Time: 0 seconds, Memory: 5.75Mb
    5. There was 1 failure:
    6. 1) DataTest::testAdd with data set #3 (1, 1, 3)
    7. Failed asserting that 2 is identical to 3.
    8. /home/sb/DataTest.php:9
    9. FAILURES!
    10. Tests: 4, Assertions: 4, Failures: 1.

    当使用到大量数据集时,最好逐个用字符串键名对其命名,避免用默认的数字键名。这样输出信息会更加详细些,其中将包含打断测试的数据集所对应的名称。

    示例 2.6 将数据供给器与命名数据集一起使用

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class DataTest extends TestCase
    4. {
    5. /**
    6. * @dataProvider additionProvider
    7. */
    8. public function testAdd(int $a, int $b, int $expected): void
    9. {
    10. $this->assertSame($expected, $a + $b);
    11. }
    12. public function additionProvider(): array
    13. {
    14. return [
    15. 'adding zeros' => [0, 0, 0],
    16. 'zero plus one' => [0, 1, 1],
    17. 'one plus zero' => [1, 0, 1],
    18. 'one plus one' => [1, 1, 3]
    19. ];
    20. }
    21. }
    1. $ phpunit DataTest
    2. PHPUnit latest.0 by Sebastian Bergmann and contributors.
    3. ...F
    4. Time: 0 seconds, Memory: 5.75Mb
    5. There was 1 failure:
    6. 1) DataTest::testAdd with data set "one plus one" (1, 1, 3)
    7. Failed asserting that 2 is identical to 3.
    8. /home/sb/DataTest.php:9
    9. FAILURES!
    10. Tests: 4, Assertions: 4, Failures: 1.

    示例 2.7 使用返回 Iterator 对象的数据供给器

    1. $ phpunit DataTest
    2. PHPUnit latest.0 by Sebastian Bergmann and contributors.
    3. ...F
    4. Time: 0 seconds, Memory: 5.75Mb
    5. 1) DataTest::testAdd with data set #3 ('1', '1', '3')
    6. /home/sb/DataTest.php:11
    7. FAILURES!
    8. Tests: 4, Assertions: 4, Failures: 1.

    示例 2.8 CsvFileIterator 类

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class CsvFileIterator implements Iterator
    4. {
    5. private $file;
    6. private $key = 0;
    7. private $current;
    8. public function __construct(string $file)
    9. {
    10. $this->file = fopen($file, 'r');
    11. }
    12. public function __destruct()
    13. {
    14. fclose($this->file);
    15. }
    16. public function rewind(): void
    17. {
    18. rewind($this->file);
    19. $this->current = fgetcsv($this->file);
    20. $this->key = 0;
    21. }
    22. public function valid(): bool
    23. {
    24. return !feof($this->file);
    25. }
    26. public function key(): int
    27. {
    28. return $this->key;
    29. }
    30. public function current(): array
    31. {
    32. return $this->current;
    33. }
    34. public function next(): void
    35. {
    36. $this->current = fgetcsv($this->file);
    37. $this->key++;
    38. }
    39. }

    如果测试同时从 @dataProvider 方法和一个或多个 @depends 测试接收数据,那么来自于数据供给器的参数将先于来自所依赖的测试的。来自于所依赖的测试的参数对于每个数据集都是一样的。参见示例 2.9

    示例 2.9 在同一个测试中组合 @depends 和 @dataProvider

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class DependencyAndDataProviderComboTest extends TestCase
    4. {
    5. public function provider(): array
    6. {
    7. return [['provider1'], ['provider2']];
    8. }
    9. public function testProducerFirst(): void
    10. {
    11. $this->assertTrue(true);
    12. return 'first';
    13. }
    14. public function testProducerSecond(): void
    15. {
    16. $this->assertTrue(true);
    17. return 'second';
    18. }
    19. /**
    20. * @depends testProducerFirst
    21. * @depends testProducerSecond
    22. * @dataProvider provider
    23. */
    24. public function testConsumer(): void
    25. {
    26. $this->assertSame(
    27. ['provider1', 'first', 'second'],
    28. func_get_args()
    29. );
    30. }
    31. }
    1. $ phpunit --verbose DependencyAndDataProviderComboTest
    2. PHPUnit latest.0 by Sebastian Bergmann and contributors.
    3. ...F
    4. Time: 0 seconds, Memory: 3.50Mb
    5. There was 1 failure:
    6. 1) DependencyAndDataProviderComboTest::testConsumer with data set #1 ('provider2')
    7. Failed asserting that two arrays are identical.
    8. --- Expected
    9. +++ Actual
    10. @@ @@
    11. Array &0 (
    12. - 0 => 'provider1'
    13. + 0 => 'provider2'
    14. 1 => 'first'
    15. 2 => 'second'
    16. )
    17. /home/sb/DependencyAndDataProviderComboTest.php:32
    18. FAILURES!
    19. Tests: 4, Assertions: 4, Failures: 1.

    示例 2.10 对单个测试使用多个数据供给器

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class DataTest extends TestCase
    4. {
    5. /**
    6. * @dataProvider additionWithNonNegativeNumbersProvider
    7. * @dataProvider additionWithNegativeNumbersProvider
    8. */
    9. public function testAdd(int $a, int $b, int $expected): void
    10. {
    11. $this->assertSame($expected, $a + $b);
    12. }
    13. public function additionWithNonNegativeNumbersProvider(): void
    14. {
    15. return [
    16. [0, 1, 1],
    17. [1, 0, 1],
    18. [1, 1, 3]
    19. ];
    20. }
    21. public function additionWithNegativeNumbersProvider(): array
    22. {
    23. return [
    24. [-1, 1, 0],
    25. [-1, -1, -2],
    26. [1, -1, 0]
    27. ];
    28. }
    29. }
    1. $ phpunit DataTest
    2. PHPUnit latest.0 by Sebastian Bergmann and contributors.
    3. ..F... 6 / 6 (100%)
    4. Time: 0 seconds, Memory: 5.75Mb
    5. There was 1 failure:
    6. 1) DataTest::testAdd with data set #3 (1, 1, 3)
    7. Failed asserting that 2 is identical to 3.
    8. /home/sb/DataTest.php:12
    9. FAILURES!
    10. Tests: 6, Assertions: 6, Failures: 1.

    如果一个测试依赖于另外一个使用了数据供给器的测试,仅当被依赖的测试至少能在一组数据上成功时,依赖于它的测试才会运行。使用了数据供给器的测试,其运行结果是无法注入到依赖于此测试的其他测试中的。

    所有数据供给器方法的执行都是在对 setUpBeforeClass() 静态方法的调用和第一次对 setUp() 方法的调用之前完成的。因此,无法在数据供给器中使用创建于这两个方法内的变量。这是必须的,这样 PHPUnit 才能计算测试的总数量。

    展示了如何用 @expectException 标注来测试被测代码中是否抛出了异常。

    示例 2.11 使用 expectException() 方法

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class ExceptionTest extends TestCase
    4. {
    5. public function testException(): void
    6. {
    7. $this->expectException(InvalidArgumentException::class);
    8. }
    9. }
    1. $ phpunit ExceptionTest
    2. PHPUnit latest.0 by Sebastian Bergmann and contributors.
    3. F
    4. Time: 0 seconds, Memory: 4.75Mb
    5. There was 1 failure:
    6. 1) ExceptionTest::testException
    7. Failed asserting that exception of type "InvalidArgumentException" is thrown.
    8. FAILURES!
    9. Tests: 1, Assertions: 1, Failures: 1.

    除了 expectException() 方法外,还有 expectExceptionCode()expectExceptionMessage()expectExceptionMessageMatches() 方法可以用于为被测代码所抛出的异常建立预期。

    注意 expectExceptionMessage() 断言的是 $actual 讯息包含有 $expected 讯息,并不执行精确的字符串比较。

    对 PHP 错误、警告和通知进行测试

    默认情况下,PHPUnit 将测试在执行中触发的 PHP 错误、警告、通知都转换为异常。先不说其他好处,这样就可以预期在测试中会触发 PHP 错误、警告或通知,如示例 2.12 所示。

    PHP 的 error_reporting 运行时配置会对 PHPUnit 将哪些错误转换为异常有所限制。如果在这个特性上碰到问题,请确认 PHP 的配置中没有抑制你所关注的错误类型。

    示例 2.12 预期会出现 PHP 错误、警告和通知

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class ErrorTest extends TestCase
    4. {
    5. public function testDeprecationCanBeExpected(): void
    6. {
    7. $this->expectDeprecation();
    8. // (可选)测试讯息和某个字符串相等
    9. $this->expectDeprecationMessage('foo');
    10. // 或者(可选)测试讯息和某个正则表达式匹配
    11. $this->expectDeprecationMessageMatches('/foo/');
    12. \trigger_error('foo', \E_USER_DEPRECATED);
    13. }
    14. public function testNoticeCanBeExpected(): void
    15. {
    16. // (可选)测试讯息和某个字符串相等
    17. $this->expectNoticeMessage('foo');
    18. // 或者(可选)测试讯息和某个正则表达式匹配
    19. $this->expectNoticeMessageMatches('/foo/');
    20. \trigger_error('foo', \E_USER_NOTICE);
    21. }
    22. public function testWarningCanBeExpected(): void
    23. {
    24. $this->expectWarning();
    25. // (可选)测试讯息和某个字符串相等
    26. $this->expectWarningMessage('foo');
    27. // 或者(可选)测试讯息和某个正则表达式匹配
    28. $this->expectWarningMessageMatches('/foo/');
    29. \trigger_error('foo', \E_USER_WARNING);
    30. }
    31. public function testErrorCanBeExpected(): void
    32. {
    33. $this->expectError();
    34. // (可选)测试讯息和某个字符串相等
    35. $this->expectErrorMessage('foo');
    36. // 或者(可选)测试讯息和某个正则表达式匹配
    37. $this->expectErrorMessageMatches('/foo/');
    38. \trigger_error('foo', \E_USER_ERROR);
    39. }
    40. }

    如果测试代码使用了会触发错误的 PHP 内建函数,比如 fopen,有时候在测试中使用错误抑制符会很有用。通过抑制住错误通知,就能对返回值进行检查,否则错误通知将会导致 PHPUnit 的错误处理程序抛出异常。

    示例 2.13 对会引发PHP 错误的代码的返回值进行测试

    1. $ phpunit ErrorSuppressionTest
    2. PHPUnit latest.0 by Sebastian Bergmann and contributors.
    3. .
    4. Time: 1 seconds, Memory: 5.25Mb
    5. OK (1 test, 1 assertion)

    如果不使用错误抑制符,此测试将会失败,并报告 fopen(/is-not-writeable/file): failed to open stream: No such file or directory

    有时候,想要断言(比如说)某方法的运行过程中生成了预期的输出(例如,通过 echoprint)。PHPUnit\Framework\TestCase 类使用 PHP 的特性来为此提供必要的功能支持。

    示例 2.14 展示了如何用 expectOutputString() 方法来设定所预期的输出。如果没有产生预期的输出,测试将计为失败。

    示例 2.14 对函数或方法的输出进行测试

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class OutputTest extends TestCase
    4. {
    5. public function testExpectFooActualFoo(): void
    6. {
    7. $this->expectOutputString('foo');
    8. print 'foo';
    9. }
    10. public function testExpectBarActualBaz(): void
    11. {
    12. $this->expectOutputString('bar');
    13. print 'baz';
    14. }
    15. }
    1. $ phpunit OutputTest
    2. PHPUnit latest.0 by Sebastian Bergmann and contributors.
    3. .F
    4. Time: 0 seconds, Memory: 5.75Mb
    5. There was 1 failure:
    6. 1) OutputTest::testExpectBarActualBaz
    7. Failed asserting that two strings are equal.
    8. --- Expected
    9. +++ Actual
    10. @@ @@
    11. -'bar'
    12. +'baz'
    13. FAILURES!
    14. Tests: 2, Assertions: 2, Failures: 1.

    中列举了用于对输出进行测试的各种方法

    在严格模式下,本身产生输出的测试将会失败。

    错误相关信息的输出

    当有测试失败时,PHPUnit 全力提供尽可能多的有助于找出问题所在的上下文信息。

    示例 2.15 数组比较失败时生成的错误输出

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class ArrayDiffTest extends TestCase
    4. {
    5. public function testEquality(): void
    6. {
    7. $this->assertSame(
    8. [1, 2, 3, 4, 5, 6],
    9. [1, 2, 33, 4, 5, 6]
    10. );
    11. }
    12. }
    1. $ phpunit ArrayDiffTest
    2. PHPUnit latest.0 by Sebastian Bergmann and contributors.
    3. F
    4. Time: 0 seconds, Memory: 5.25Mb
    5. There was 1 failure:
    6. 1) ArrayDiffTest::testEquality
    7. Failed asserting that two arrays are identical.
    8. --- Expected
    9. +++ Actual
    10. @@ @@
    11. Array (
    12. 0 => 1
    13. 1 => 2
    14. - 2 => 3
    15. + 2 => 33
    16. 3 => 4
    17. 4 => 5
    18. 5 => 6
    19. )
    20. /home/sb/ArrayDiffTest.php:7
    21. FAILURES!
    22. Tests: 1, Assertions: 1, Failures: 1.

    在这个例子中,数组中只有一个值不同,但其他值也都同时显示出来,以提供关于错误发生的位置的上下文信息。

    当生成的输出很长而难以阅读时,PHPUnit 将对其进行分割,并在每个差异附近提供少数几行上下文信息。

    示例 2.16 长数组的数组比较失败时的错误输出

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class LongArrayDiffTest extends TestCase
    4. {
    5. public function testEquality(): void
    6. {
    7. $this->assertSame(
    8. [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6],
    9. [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 33, 4, 5, 6]
    10. );
    11. }
    12. }
    1. $ phpunit LongArrayDiffTest
    2. PHPUnit latest.0 by Sebastian Bergmann and contributors.
    3. F
    4. Time: 0 seconds, Memory: 5.25Mb
    5. There was 1 failure:
    6. 1) LongArrayDiffTest::testEquality
    7. Failed asserting that two arrays are identical.
    8. --- Expected
    9. +++ Actual
    10. @@ @@
    11. 11 => 0
    12. 12 => 1
    13. 13 => 2
    14. - 14 => 3
    15. + 14 => 33
    16. 15 => 4
    17. 16 => 5
    18. 17 => 6
    19. )
    20. /home/sb/LongArrayDiffTest.php:7
    21. FAILURES!
    22. Tests: 1, Assertions: 1, Failures: 1.

    当比较失败时,PHPUnit 为输入值建立文本表示,然后以此进行对比。这种实现导致在差异指示中显示出来的问题可能比实际上存在的多。

    这种情况只出现在对数组或者对象使用 assertEquals() 或其他“弱”比较函数时。

    示例 2.17 使用弱比较时在差异生成过程中的边缘情况

    1. <?php declare(strict_types=1);
    2. use PHPUnit\Framework\TestCase;
    3. final class ArrayWeakComparisonTest extends TestCase
    4. {
    5. public function testEquality(): void
    6. {
    7. $this->assertEquals(
    8. [1, 2, 3, 4, 5, 6],
    9. ['1', 2, 33, 4, 5, 6]
    10. );
    11. }
    12. }
    1. $ phpunit ArrayWeakComparisonTest
    2. PHPUnit latest.0 by Sebastian Bergmann and contributors.
    3. F
    4. Time: 0 seconds, Memory: 5.25Mb
    5. There was 1 failure:
    6. 1) ArrayWeakComparisonTest::testEquality
    7. Failed asserting that two arrays are equal.
    8. --- Expected
    9. +++ Actual
    10. @@ @@
    11. Array (
    12. - 0 => 1
    13. + 0 => '1'
    14. 1 => 2
    15. - 2 => 3
    16. + 2 => 33
    17. 3 => 4
    18. 4 => 5
    19. 5 => 6
    20. )
    21. /home/sb/ArrayWeakComparisonTest.php:7
    22. Tests: 1, Assertions: 1, Failures: 1.

    在这个例子中,第一个索引项中的 1 和 在报告中被视为不同,虽然 assertEquals() 认为这两个值是匹配的。