非常教程

Phpunit 6参考手册

指南 | Guides

Writing Tests for PHPUnit

例2.1展示了我们如何使用PHPUnit来编写测试,这些测试使用PHP的数组操作。该示例介绍了使用PHPUnit编写测试的基本约定和步骤:

  1. 类Class的测试进入类ClassTest。
  2. ClassTest从PHPUnit \ Framework \ TestCase继承(大部分时间)。
  3. 测试是名为test *的公共方法。或者,您可以在方法的docblock中使用@test注释将其标记为测试方法。
  4. 在测试方法内部,断言方法(如assertEquals()(参见附录A))用于声明实际值与期望值匹配。

例2.1:用PHPUnit测试数组操作

<?php
use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    public function testPushAndPop()
    {
        $stack = [];
        $this->assertEquals(0, count($stack));

        array_push($stack, 'foo');
        $this->assertEquals('foo', $stack[count($stack)-1]);
        $this->assertEquals(1, count($stack));

        $this->assertEquals('foo', array_pop($stack));
        $this->assertEquals(0, count($stack));
    }
}
?>

每当你想要在打印语句或调试器表达式中输入某些东西时,就把它写成一个测试。

- 马丁福勒

测试依赖关系

单元测试主要是作为一种良好的实践来编写的,以帮助开发人员识别和修复错误,重构代码并作为被测软件单元的文档。为了获得这些好处,理想的单元测试应该覆盖程序中所有可能的路径。一个单元测试通常覆盖一个函数或方法中的一个特定路径。然而,测试方法不是封装的独立实体所必需的。测试方法之间通常存在隐式依赖关系,隐藏在测试的实现场景中。

- 阿德里安库恩

PHPUnit支持在测试方法之间声明明确的依赖关系。这种依赖关系不定义测试方法执行的顺序,但它们允许生产者返回测试装置的实例,并将其传递给相关消费者。

  • 生产者是一种测试方法,它将被测试的单元作为返回值。
  • 消费者是一种依赖于一个或多个生产者及其返回值的测试方法。

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

例2.2:使用 @depends 注解来表示依赖关系

<?php
use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    public function testEmpty()
    {
        $stack = [];
        $this->assertEmpty($stack);

        return $stack;
    }

    /**
     * @depends testEmpty
     */
    public function testPush(array $stack)
    {
        array_push($stack, 'foo');
        $this->assertEquals('foo', $stack[count($stack)-1]);
        $this->assertNotEmpty($stack);

        return $stack;
    }

    /**
     * @depends testPush
     */
    public function testPop(array $stack)
    {
        $this->assertEquals('foo', array_pop($stack));
        $this->assertEmpty($stack);
    }
}
?>

在上面的例子中,第一个测试testEmpty()创建一个新数组并声明它是空的。 测试然后返回夹具作为其结果。 第二个测试testPush()依赖于testEmpty()并将该依赖测试的结果作为参数传递。 最后,testPop()依赖于testPush()。

生产者产生的回报价值默认情况下是“按原样”传递给消费者的。这意味着当生产者返回一个对象时,对该对象的引用被传递给使用者。当应该使用副本而不是参考时,应该使用@depends clone而不是@depends

为了快速定位缺陷,我们希望我们的注意力集中在相关的失败测试上。这就是PHPUnit在依赖测试失败时跳过测试执行的原因。这可以通过利用例2.3中所示的测试之间的依赖关系来改进缺陷本地化。

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

<?php
use PHPUnit\Framework\TestCase;

class DependencyFailureTest extends TestCase
{
    public function testOne()
    {
        $this->assertTrue(false);
    }

    /**
     * @depends testOne
     */
    public function testTwo()
    {
    }
}
?>
phpunit --verbose DependencyFailureTest
PHPUnit 6.4.0 by Sebastian Bergmann and contributors.

FS

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) DependencyFailureTest::testOne
Failed asserting that false is true.

/home/sb/DependencyFailureTest.php:6

There was 1 skipped test:

1) DependencyFailureTest::testTwo
This test depends on "DependencyFailureTest::testOne" to pass.


FAILURES!
Tests: 1, Assertions: 1, Failures: 1, Skipped: 1.

测试可能有多个@depends注释。PHPUnit不会更改执行测试的顺序,您必须确保在测试运行之前实际可以满足测试的依赖关系。

具有多个@depends注释的测试将从第一个参数生成第一个参数,第二个参数生成器作为第二个参数,依此类推。见例2.4

例2.4:测试多个依赖关系

<?php
use PHPUnit\Framework\TestCase;

class MultipleDependenciesTest extends TestCase
{
    public function testProducerFirst()
    {
        $this->assertTrue(true);
        return 'first';
    }

    public function testProducerSecond()
    {
        $this->assertTrue(true);
        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     */
    public function testConsumer()
    {
        $this->assertEquals(
            ['first', 'second'],
            func_get_args()
        );
    }
}
?>
phpunit --verbose MultipleDependenciesTest
PHPUnit 6.4.0 by Sebastian Bergmann and contributors.

...

Time: 0 seconds, Memory: 3.25Mb

OK (3 tests, 3 assertions)

数据提供者

一个测试方法可以接受任意的参数。这些参数将由数据提供者方法提供(additionProvider()在例2.5中)。要使用的数据提供者方法使用@dataProvider注释来指定。

数据提供者方法必须是公共的,并且可以返回一个数组数组或一个实现Iterator接口的对象,并为每个迭代步骤生成一个数组。 对于作为集合一部分的每个数组,将以该数组的内容作为参数调用测试方法。

例2.5:使用返回数组数组的数据提供者

<?php
use PHPUnit\Framework\TestCase;

class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $this->assertEquals($expected, $a + $b);
    }

    public function additionProvider()
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 3]
        ];
    }
}
?>
phpunit DataTest
PHPUnit 6.4.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) DataTest::testAdd with data set #3 (1, 1, 3)
Failed asserting that 2 matches expected 3.

/home/sb/DataTest.php:9

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.

当使用大量数据集时,使用字符串键而不是默认数字命名每个数据集非常有用。输出将更加冗长,因为它将包含打破测试的数据集的名称。

示例2.6:使用具有指定数据集的数据提供者

<?php
use PHPUnit\Framework\TestCase;

class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $this->assertEquals($expected, $a + $b);
    }

    public function additionProvider()
    {
        return [
            'adding zeros'  => [0, 0, 0],
            'zero plus one' => [0, 1, 1],
            'one plus zero' => [1, 0, 1],
            'one plus one'  => [1, 1, 3]
        ];
    }
}
?>
phpunit DataTest
PHPUnit 6.4.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) DataTest::testAdd with data set "one plus one" (1, 1, 3)
Failed asserting that 2 matches expected 3.

/home/sb/DataTest.php:9

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.

例2.7:使用返回Iterator对象的数据提供者

<?php
use PHPUnit\Framework\TestCase;

require 'CsvFileIterator.php';

class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $this->assertEquals($expected, $a + $b);
    }

    public function additionProvider()
    {
        return new CsvFileIterator('data.csv');
    }
}
?>
phpunit DataTest
PHPUnit 6.4.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) DataTest::testAdd with data set #3 ('1', '1', '3')
Failed asserting that 2 matches expected '3'.

/home/sb/DataTest.php:11

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.

例子2.8:CsvFileIterator类

<?php
use PHPUnit\Framework\TestCase;

class CsvFileIterator implements Iterator {
    protected $file;
    protected $key = 0;
    protected $current;

    public function __construct($file) {
        $this->file = fopen($file, 'r');
    }

    public function __destruct() {
        fclose($this->file);
    }

    public function rewind() {
        rewind($this->file);
        $this->current = fgetcsv($this->file);
        $this->key = 0;
    }

    public function valid() {
        return !feof($this->file);
    }

    public function key() {
        return $this->key;
    }

    public function current() {
        return $this->current;
    }

    public function next() {
        $this->current = fgetcsv($this->file);
        $this->key++;
    }
}
?>
When a test receives input from both a `@dataProvider` method and from one or more tests it `@depends` on, the arguments from the data provider will come before the ones from depended-upon tests. The arguments from depended-upon tests will be the same for each data set. See [Example 2.9](writing-tests-for-phpunit#writing-tests-for-phpunit.data-providers.examples.DependencyAndDataProviderCombo.php) 

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

<?php
use PHPUnit\Framework\TestCase;

class DependencyAndDataProviderComboTest extends TestCase
{
    public function provider()
    {
        return [['provider1'], ['provider2']];
    }

    public function testProducerFirst()
    {
        $this->assertTrue(true);
        return 'first';
    }

    public function testProducerSecond()
    {
        $this->assertTrue(true);
        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     * @dataProvider provider
     */
    public function testConsumer()
    {
        $this->assertEquals(
            ['provider1', 'first', 'second'],
            func_get_args()
        );
    }
}
?>
phpunit --verbose DependencyAndDataProviderComboTest
PHPUnit 6.4.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 3.50Mb

There was 1 failure:

1) DependencyAndDataProviderComboTest::testConsumer with data set #1 ('provider2')
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
Array (
-    0 => 'provider1'
+    0 => 'provider2'
1 => 'first'
2 => 'second'
)

/home/sb/DependencyAndDataProviderComboTest.php:31

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.
When a test depends on a test that uses data providers, the depending test will be executed when the test it depends upon is successful for at least one data set. The result of a test that uses data providers cannot be injected into a depending test. 
All data providers are executed before both the call to the `setUpBeforeClass` static method and the first call to the `setUp` method. Because of that you can't access any variables you create there from within a data provider. This is required in order for PHPUnit to be able to compute the total number of tests. 

测试例外

例2.10显示了如何使用该expectException()方法来测试被测代码是否抛出异常。

例2.10:使用expectException()方法

<?php
use PHPUnit\Framework\TestCase;

class ExceptionTest extends TestCase
{
    public function testException()
    {
        $this->expectException(InvalidArgumentException::class);
    }
}
?>
phpunit ExceptionTest
PHPUnit 6.4.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ExceptionTest::testException
Expected exception InvalidArgumentException

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
In addition to the `expectException()` method the `expectExceptionCode()`, `expectExceptionMessage()`, and `expectExceptionMessageRegExp()` methods exist to set up expectations for exceptions raised by the code under test. 

另外,您也可以使用@expectedException@expectedExceptionCode@expectedExceptionMessage,和@expectedExceptionMessageRegExp注释设立由测试中的代码引起的异常期待。例2.11给出了一个例子。

例2.11:使用@expectedException注解

<?php
use PHPUnit\Framework\TestCase;

class ExceptionTest extends TestCase
{
    /**
     * @expectedException InvalidArgumentException
     */
    public function testException()
    {
    }
}
?>
phpunit ExceptionTest
PHPUnit 6.4.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ExceptionTest::testException
Expected exception InvalidArgumentException

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

测试PHP错误

  By default, PHPUnit converts PHP errors, warnings, and notices that are triggered during the execution of a test to an exception. Using these exceptions, you can, for instance, expect a test to trigger a PHP error as shown in [Example 2.12](writing-tests-for-phpunit#writing-tests-for-phpunit.exceptions.examples.ErrorTest.php). 

PHP的error_reporting运行时配置可以限制PHPUnit将转换为异常的错误。如果您在使用此功能时遇到问题,请确保PHP未配置为禁止您正在测试的错误类型。

例2.12:使用@expectedException PHP错误

<?php
use PHPUnit\Framework\TestCase;

class ExpectedErrorTest extends TestCase
{
    /**
     * @expectedException PHPUnit\Framework\Error
     */
    public function testFailingInclude()
    {
        include 'not_existing_file.php';
    }
}
?>
phpunit -d error_reporting=2 ExpectedErrorTest
PHPUnit 6.4.0 by Sebastian Bergmann and contributors.

.

Time: 0 seconds, Memory: 5.25Mb

OK (1 test, 1 assertion)

PHPUnit\Framework\Error\NoticePHPUnit\Framework\Error\Warning分别代表PHP通知和警告。

测试异常时应尽可能具体。对过于通用的类进行测试可能会导致不良的副作用。因此,Exception使用@expectedExceptionsetExpectedException()不再允许测试该课程。

当测试依赖php函数触发错误fopen时,在测试时使用错误抑制有时会很有用。这允许您通过抑制导致phpunit的通知来检查返回值PHPUnit\Framework\Error\Notice

例2.13:测试使用PHP错误的代码的返回值

<?php
use PHPUnit\Framework\TestCase;

class ErrorSuppressionTest extends TestCase
{
    public function testFileWriting() {
        $writer = new FileWriter;
        $this->assertFalse(@$writer->write('/is-not-writeable/file', 'stuff'));
    }
}
class FileWriter
{
    public function write($file, $content) {
        $file = fopen($file, 'w');
        if($file == false) {
            return false;
        }
        // ...
    }
}

?>
phpunit ErrorSuppressionTest
PHPUnit 6.4.0 by Sebastian Bergmann and contributors.

.

Time: 1 seconds, Memory: 5.25Mb

OK (1 test, 1 assertion)

如果没有错误抑制,测试会报告失败fopen(/is-not-writeable/file): failed to open stream: No such file or directory

测试输出

例如,有时候你想要声明一个方法的执行,例如,生成一个期望的输出(例如通过echo或print)。 PHPUnit \ Framework \ TestCase类使用PHP的输出缓冲功能来提供必要的功能。

例2.14显示了如何使用该expectOutputString()方法来设置预期输出。如果未产生此预期输出,则测试将被视为失败。

例2.14:测试函数或方法的输出

<?php
use PHPUnit\Framework\TestCase;

class OutputTest extends TestCase
{
    public function testExpectFooActualFoo()
    {
        $this->expectOutputString('foo');
        print 'foo';
    }

    public function testExpectBarActualBaz()
    {
        $this->expectOutputString('bar');
        print 'baz';
    }
}
?>
phpunit OutputTest
PHPUnit 6.4.0 by Sebastian Bergmann and contributors.

.F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) OutputTest::testExpectBarActualBaz
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'


FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

表2.1显示了为测试输出提供的方法

表2.1。测试输出的方法

方法

含义

void expectOutputRegex(string $ regularExpression)

设置输出与$ regularExpression匹配的期望。

void expectOutputString(string $ expectedString)

设置输出等于$ expectedString的期望值。

bool setOutputCallback(callable $callback)

设置用于例如标准化实际输出的回调。

string getActualOutput()

获取实际输出。

发出输出的测试将在严格模式下失败。

错误输出

每当测试失败时,PHPUnit会尽可能为您提供尽可能多的上下文,以帮助识别问题。

例2.15:数组比较失败时产生的错误输出

<?php
use PHPUnit\Framework\TestCase;

class ArrayDiffTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [1, 2,  3, 4, 5, 6],
            [1, 2, 33, 4, 5, 6]
        );
    }
}
?>
phpunit ArrayDiffTest
PHPUnit 6.4.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) ArrayDiffTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
     0 => 1
     1 => 2
-    2 => 3
+    2 => 33
     3 => 4
     4 => 5
     5 => 6
 )

/home/sb/ArrayDiffTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

在这个例子中,只有一个数组值是不同的,其他值显示的是提供错误发生位置的上下文。

当生成的输出会很长时间读取时,PHPUnit会将其分解并为每个差异提供几行上下文。

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

<?php
use PHPUnit\Framework\TestCase;

class LongArrayDiffTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2,  3, 4, 5, 6],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 33, 4, 5, 6]
        );
    }
}
?>
phpunit LongArrayDiffTest
PHPUnit 6.4.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) LongArrayDiffTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
     13 => 2
-    14 => 3
+    14 => 33
     15 => 4
     16 => 5
     17 => 6
 )


/home/sb/LongArrayDiffTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

边缘情况

当比较失败时,PHPUnit会创建输入值的文本表示并对其进行比较。由于该实施,差异可能会显示比实际存在更多的问题。

这只会在数组或对象上使用assertEquals或其他“弱”比较函数时发生。

例2.17:使用弱比较时差异产生的边缘情况

<?php
use PHPUnit\Framework\TestCase;

class ArrayWeakComparisonTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [1, 2, 3, 4, 5, 6],
            ['1', 2, 33, 4, 5, 6]
        );
    }
}
?>
phpunit ArrayWeakComparisonTest
PHPUnit 6.4.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) ArrayWeakComparisonTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
-    0 => 1
+    0 => '1'
     1 => 2
-    2 => 3
+    2 => 33
     3 => 4
     4 => 5
     5 => 6
 )


/home/sb/ArrayWeakComparisonTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

在这个例子中,即使assertEquals认为这些值是匹配的1'1'也会报告第一个和第二个索引之间的差异。

Phpunit 6

PHPUnit 是一个 xUnit 的体系结构的 PHP 单元测试框架。

主页 https://phpunit.de/
源码 https://github.com/sebastianbergmann/phpunit
版本 6
发布版本 6.4

Phpunit 6目录

1.指南 | Guides
2.注释 | Annotations
3.声明 | Assertions