目录

安装phpunit

phar

$ wget https://phar.phpunit.de/phpunit-latest.phar
$ mv phpunit-latest.phar phpunit
$ chmod +x phpunit
$ ./phpunit --version
PHPUnit x.y.z by Sebastian Bergmann and contributors.

composer

composer require --dev phpunit/phpunit ^latest

执行

PS I:\src\php-shiyanchang> .\vendor\bin\phpunit --version
PHPUnit 7.0.0 by Sebastian Bergmann and contributors.

编写phpunit测试代码

依赖关系

生产者(producer),是能生成被测单元并将其作为返回值的测试方法。

消费者(consumer),是依赖于一个或多个生产者及其返回值的测试方法。

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

declare(strict_types=1);
use \PHPUnit\Framework\TestCase;

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

        return $stack;
    }

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

        return $stack;
    }
    
    /**
     * @return string
     */
    public function testGetName() :string
    {        
        $string = "tsingchan";
        $this->assertStringStartsWith("tsing", $string);
        return $string;
    }

    /**
     * @depends testPush
     * @depends testGetName
     */
    public function testPop(array $stack,string $name): void
    {
        $stack[]=$name;
        $this->assertSame('tsingchan', array_pop($stack));
        $this->assertSame('foo', array_pop($stack));
        $this->assertEmpty($stack);
//        $this->assertNotEmpty($stack);
    }
}

数据供给器

用 @dataProvider 标注来指定要使用的数据供给器方法。

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

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

declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider1
     * @dataProvider additionProvider2
     */
    public function testAdd(int $a, int $b, int $expected): void
    {
        $this->assertSame($expected, $a + $b);
    }

    public function additionProvider1(): array
    {
        return [
            'adding zeros'  => [0, 0, 0],
            'zero plus one' => [0, 1, 1],
            'one plus zero' => [1, 0, 1],
            'one plus one'  => [1, 1, 3]
        ];
    }

    public function additionProvider2(): array
    {
        return [
            '0+0'  => [1, 2, 3],
            '0+1' => [2, 2, 4],
            '2+3' => [2, 3, 5],
            '3+2'  => [3, 2, 5]
        ];
    }
}

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

异常判断

用 @expectException 标注来测试被测代码中是否抛出了异常。也可以使用方法:$this->expectException(TypeError::class);

declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class ExceptionTest extends TestCase
{
    /**
     * @expectedException TypeError
     * @return void
     */
    public function testException(): void
    {
        // $this->expectException(TypeError::class); //注解 @exceptionException和这行的效果是一样的
        
        $this->add("a");//这里可以调用业务逻辑类的代码,这里add只是为了范例方便
    }
    
    public function add(int $num):int{
        return $num;
    }
}

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

对输出进行测试

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

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

declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class OutputTest extends TestCase
{
    public function testExpectFooActualFoo(): void
    {
        $this->expectOutputString('foo');

        print 'foo';
    }

    public function testExpectBarActualBaz(): void
    {
        $this->expectOutputString('bar');

        print 'baz';
    }
}

执行phpunit命令行

命令行选项

$ phpunit --help
PHPUnit latest.0 by Sebastian Bergmann and contributors.

Usage:
  phpunit [options] UnitTest.php
  phpunit [options] <directory>

Code Coverage Options:
  --coverage-clover <file>    Generate code coverage report in Clover XML format
  --coverage-crap4j <file>    Generate code coverage report in Crap4J XML format
  --coverage-html <dir>       Generate code coverage report in HTML format
  --coverage-php <file>       Export PHP_CodeCoverage object to file
  --coverage-text <file>      Generate code coverage report in text format [default: standard output]
  --coverage-xml <dir>        Generate code coverage report in PHPUnit XML format
  --coverage-cache <dir>      Cache static analysis results
  --warm-coverage-cache       Warm static analysis cache
  --coverage-filter <dir>     Include <dir> in code coverage analysis
  --path-coverage             Perform path coverage analysis
  --disable-coverage-ignore   Disable annotations for ignoring code coverage
  --no-coverage               Ignore code coverage configuration

Logging Options:
  --log-junit <file>          Log test execution in JUnit XML format to file
  --log-teamcity <file>       Log test execution in TeamCity format to file
  --testdox-html <file>       Write agile documentation in HTML format to file
  --testdox-text <file>       Write agile documentation in Text format to file
  --testdox-xml <file>        Write agile documentation in XML format to file
  --reverse-list              Print defects in reverse order
  --no-logging                Ignore logging configuration

Test Selection Options:
  --filter <pattern>          Filter which tests to run
  --testsuite <name>          Filter which testsuite to run
  --group <name>              Only runs tests from the specified group(s)
  --exclude-group <name>      Exclude tests from the specified group(s)
  --list-groups               List available test groups
  --list-suites               List available test suites
  --list-tests                List available tests
  --list-tests-xml <file>     List available tests in XML format
  --test-suffix <suffixes>    Only search for test in files with specified suffix(es). Default: Test.php,.phpt

Test Execution Options:
  --dont-report-useless-tests Do not report tests that do not test anything
  --strict-coverage           Be strict about @covers annotation usage
  --strict-global-state       Be strict about changes to global state
  --disallow-test-output      Be strict about output during tests
  --disallow-resource-usage   Be strict about resource usage during small tests
  --enforce-time-limit        Enforce time limit based on test size
  --default-time-limit <sec>  Timeout in seconds for tests without @small, @medium or @large
  --disallow-todo-tests       Disallow @todo-annotated tests

  --process-isolation         Run each test in a separate PHP process
  --globals-backup            Backup and restore $GLOBALS for each test
  --static-backup             Backup and restore static attributes for each test

  --colors <flag>             Use colors in output ("never", "auto" or "always")
  --columns <n>               Number of columns to use for progress output
  --columns max               Use maximum number of columns for progress output
  --stderr                    Write to STDERR instead of STDOUT
  --stop-on-defect            Stop execution upon first not-passed test
  --stop-on-error             Stop execution upon first error
  --stop-on-failure           Stop execution upon first error or failure
  --stop-on-warning           Stop execution upon first warning
  --stop-on-risky             Stop execution upon first risky test
  --stop-on-skipped           Stop execution upon first skipped test
  --stop-on-incomplete        Stop execution upon first incomplete test
  --fail-on-incomplete        Treat incomplete tests as failures
  --fail-on-risky             Treat risky tests as failures
  --fail-on-skipped           Treat skipped tests as failures
  --fail-on-warning           Treat tests with warnings as failures
  -v|--verbose                Output more verbose information
  --debug                     Display debugging information

  --repeat <times>            Runs the test(s) repeatedly
  --teamcity                  Report test execution progress in TeamCity format
  --testdox                   Report test execution progress in TestDox format
  --testdox-group             Only include tests from the specified group(s)
  --testdox-exclude-group     Exclude tests from the specified group(s)
  --no-interaction            Disable TestDox progress animation
  --printer <printer>         TestListener implementation to use

  --order-by <order>          Run tests in order: default|defects|duration|no-depends|random|reverse|size
  --random-order-seed <N>     Use a specific random seed <N> for random order
  --cache-result              Write test results to cache file
  --do-not-cache-result       Do not write test results to cache file

Configuration Options:
  --prepend <file>            A PHP script that is included as early as possible
  --bootstrap <file>          A PHP script that is included before the tests run
  -c|--configuration <file>   Read configuration from XML file
  --no-configuration          Ignore default configuration file (phpunit.xml)
  --extensions <extensions>   A comma separated list of PHPUnit extensions to load
  --no-extensions             Do not load PHPUnit extensions
  --include-path <path(s)>    Prepend PHP's include_path with given path(s)
  -d <key[=value]>            Sets a php.ini value
  --cache-result-file <file>  Specify result cache path and filename
  --generate-configuration    Generate configuration file with suggested settings
  --migrate-configuration     Migrate configuration file to current format

Miscellaneous Options:
  -h|--help                   Prints this usage information
  --version                   Prints the version and exits
  --atleast-version <min>     Checks that version is greater than min and exits
  --check-version             Check whether PHPUnit is the latest version

TestDox

组织测试方式

支持文件或XML编排测试套件

5. 组织测试 — PHPUnit latest 手册

如果有IDE集成了测试套件,IDE上就会有个按钮是执行所有测试类文件。

跳过测试

用 @requires 来跳过测试

类型 可能值 示例 其他示例
PHP 任意 PHP 版本号以及可选的运算符 @requires PHP 7.1.20 @requires PHP >= 7.2
PHPUnit 任意 PHPUnit 版本号以及可选的运算符 @requires PHPUnit 7.3.1 @requires PHPUnit < 8
OS 与 PHP_OS 匹配的正则表达式 @requires OS Linux @requires OS WIN32
OSFAMILY 任意 OS family @requires OSFAMILY Solaris @requires OSFAMILY Windows
function 任意 function_exists 的有效参数 @requires function imap_open @requires function ReflectionMethod::setAccessible
extension 任意扩展名以及可选的版本号和可选的运算符 @requires extension mysqli @requires extension redis >= 2.2.0

HP、PHPUnit 和扩展的版本约束支持以下运算符:<、<=、>、>=、=、==、!=、<>。

版本是用 PHP 的 version_compare 函数进行比较的。除了其他事情之外,这意味着 = 和 == 运算符只能用于完整的 X.Y.Z 版本号,只用 X.Y 是不行的。

use PHPUnit\Framework\TestCase;

/**
 * @requires extension mysqli
 */
final class DatabaseTest extends TestCase
{
    /**
     * @requires PHP >= 7.0
     */
    public function testConnection(): void
    {
        // 测试需要 mysqli 扩展,并且要求 PHP >= 7.0
    }

    // ... 其他需要 mysqli 扩展的测试
}

测试替身

有时候对被测系统进行测试是很困难的,因为它依赖于其他无法在测试环境中使用的组件。这有可能是因为这些组件不可用,它们不会返回测试所需要的结果,或者执行它们会有不良副作用。

如果在编写测试时无法使用(或选择不使用)实际的依赖组件,可以用测试替身来代替。测试替身不需要和真正的依赖组件有完全一样的的行为方式;它只需要提供和真正的组件同样的 API 即可,这样被测系统就会以为它是真正的组件!

PHPUnit 提供的 createStub($type)、createMock($type) 和 getMockBuilder($type) 方法可以在测试中用来自动生成对象,此对象可以充当任意指定原版类型(接口或类名)的测试替身。在任何预期或要求使用原版类的实例对象的上下文中都可以使用这个测试替身对象来代替。

createStub($type) 和 createMock($type) 方法直接返回指定类型(接口或类)的测试替身对象实例。

打桩Stubs

将对象替换为(可选地)返回配置好的返回值的测试替身的实践方法称为打桩(stubbing)。可以用桩件(Stub)来“替换掉被测系统所依赖的实际组件,这样测试就有了对被测系统的间接输入的控制点。这使得测试能强制安排被测系统的执行路径,否则被测系统可能无法执行”

业务逻辑类,上桩类:SomeClass:

declare(strict_types=1);
class SomeClass
{
    public function doSomething()
    {
        // 随便做点什么。
    }
}

测试类对某个方法的调用进行上桩,返回固定值:

declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class StubTest extends TestCase
{
    public function testStub(): void
    {
        // 为 SomeClass 类创建桩件。
        $stub = $this->createStub(SomeClass::class);

        // 配置桩件。
        $stub->method('doSomething')
             ->willReturn('foo');

        // 现在调用 $stub->doSomething() 会返回 'foo'。
        $this->assertSame('foo', $stub->doSomething());
    }

用 willReturn($value) 返回简单值。这个简短的语法相当于 will($this->returnValue($value))。而在这个长点的语法中,可以使用变量,从而实现更复杂的上桩行为。

用returnArgument返回方法参数:

declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class StubTest extends TestCase
{
    public function testReturnArgumentStub(): void
    {
        // 为 SomeClass 类创建桩件。
        $stub = $this->createStub(SomeClass::class);

        // 配置桩件。
        $stub->method('doSomething')
             ->will($this->returnArgument(0));

        // $stub->doSomething('foo') 返回 'foo'
        $this->assertSame('foo', $stub->doSomething('foo'));

        // $stub->doSomething('bar') 返回 'bar'
        $this->assertSame('bar', $stub->doSomething('bar'));
    }
}  

用 returnSelf()返回桩对象:

declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class StubTest extends TestCase
{
    public function testReturnSelf(): void
    {
        // 为 SomeClass 类创建桩件。
        $stub = $this->createStub(SomeClass::class);

        // 配置桩件。
        $stub->method('doSomething')
             ->will($this->returnSelf());

        // $stub->doSomething() 返回 $stub
        $this->assertSame($stub, $stub->doSomething());
    }
}

更多对方法的调用上桩的方式详见:8. 测试替身 — PHPUnit latest 手册

仿件对象(Mock Object)

仿件对象是为了模仿被调用对象。

业务逻辑代码采用了观察者模式:

declare(strict_types=1);
use PHPUnit\Framework\TestCase;

class Subject
{
    protected $observers = [];
    protected $name;

    public function __construct($name)
    {
        $this->name = $name;
    }

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

    public function attach(Observer $observer)
    {
        $this->observers[] = $observer;
    }

    public function doSomething()
    {
        // 随便做点什么。
        // ...

        // 通知观察者我们做了点什么。
        $this->notify('something');
    }

    public function doSomethingBad()
    {
        foreach ($this->observers as $observer) {
            $observer->reportError(42, 'Something bad happened', $this);
        }
    }

    protected function notify($argument)
    {
        foreach ($this->observers as $observer) {
            $observer->update($argument);
        }
    }

    // 其他方法。
}

class Observer
{
    public function update($argument)
    {
        // 随便做点什么。
    }

    public function reportError($errorCode, $errorMessage, Subject $subject)
    {
        // 随便做点什么
    }

    // 其他方法。
}

首先用 PHPUnit\Framework\TestCase 类提供的 createMock() 方法来为 Observer 建立仿件对象。

由于关注的是检验某个方法是否被调用,以及调用时具体所使用的参数,因此引入 expects() 与 with() 方法来指明此交互应该是什么样的。

php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class SubjectTest extends TestCase
{
    public function testObserversAreUpdated(): void
    {
        // 为 Observer 类建立仿件
        // 只模仿 update() 方法。
        $observer = $this->createMock(Observer::class);

        // 为 update() 方法建立预期:
        // 只会以字符串 'something' 为参数调用一次。
        $observer->expects($this->once())
                 ->method('update')
                 ->with($this->equalTo('something'));

        // 建立 Subject 对象并且将模仿的 Observer 对象附加其上。
        $subject = new Subject('My subject');
        $subject->attach($observer);

        // 在 $subject 上调用 doSomething() 方法,
        // 我们预期会以字符串 'something' 调用模仿的 Observer
        // 对象的 update() 方法。
        $subject->doSomething();
    }
}

参考

PHPUnit 手册 — PHPUnit latest 手册