概述
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。 –百度百科
安装phpunit
###
composer require --dev phpunit/phpunit ^6.2
测试demo
class Transfer {
private $accountA = 100;
private $accountB = 100;
public function aToB(int $money)
{
$this->accountA -= $money;
$this->accountB += $money;
}
public function bToA(int $money)
{
$this->accountB -= $money;
$this->accountA += $money;
}
public function getAccountA()
{
return $this->accountA;
}
public function getAccountB()
{
return $this->accountB;
}
}
class TestTransfer extends TestCase{
private $transferObj;
public function setUp()
{
$this->transferObj = new Transfer();
}
public function testAtoB()
{
$originalA = $this->transferObj->getAccountA();
$originalB = $this->transferObj->getAccountB();
$this->transferObj->aToB(10);
$this->assertEquals($originalA - 10, $this->transferObj->getAccountA());
$this->assertEquals($originalB + 10, $this->transferObj->getAccountB());
}
}
执行测试:
./vendor/bin/phpunit index.php
PHPUnit 7.5.1 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 543 ms, Memory: 4.00MB
OK (1 test, 2 assertions)
或者使用 phpunit.phar
wget http://phar.phpunit.cn/phpunit.phar
chmod +x phpunit.phar
sudo mv phpunit.phar /usr/local/bin/phpunit
phpunit --version
phpunit index.php
详细使用
基境
PHPUnit 支持共享建立基境的代码。
提供了以下几个模板方法:
- setUpBeforeClass : 测试用例类的第一个测试运行之前执行
- tearDownAfterClass : 测试用例类的最后一个测试运行之后执行
- setUp : 每个测试方法运行之前执行
- tearDown : 每个测试方法运行之后执行
==注意:每个测试方法都是在一个全新的测试类实例上运行的==
全局状态
- 全局变量:有时候测试代码中用到了全局变量($_GLOBALS),但是如果对这里面的变量进行了修改,可能会导致其他测试方法出现问题,那么怎么保证每个测试方法都使用的是一样的全局变量呢? 通过:
@backupGlobals disabled|enabled
它可标注在:- 测试类 : 作用范围为整个测试类
- 测试方法 : 作用范围为这个方法
/** * @backupGlobals disabled */ class MyTest extends TestCase { // ... } /** * @backupGlobals disabled */ class MyTest extends TestCase { /** * @backupGlobals enabled */ public function testThatInteractsWithGlobalVariables() { // ... } }
支持设置 “全局变量黑名单” 黑名单中的全局变量将被排除于备份与还原操作之外:
class MyTest extends TestCase { protected $backupGlobalsBlacklist = ['globalVariable']; // ... }
对于全局变量的备份和还原的原理是使用了:
serialize()
与unserialize()
。注意:
- 对于无法被序列化的对象放入 $GLOBALS 数组内时,备份操作就会出问题。比如:
PDO
- 在方法(例如
setUp()
)内对$backupGlobalsBlacklist
属性进行设置是无效的
-
类的静态属性 。对于类的静态属性的备份和还原可以通过:
@backupStaticAttributes enabled|disabled
作用对象:在测试开始时已声明的所有类(而不仅是测试类自身),且只作用于静态类属性,不作用于函数内声明的静态变量。
使用位置和
backupGlobals
一致:- 测试类
- 测试方法
只有启用了
@backupStaticAttributes
的测试方法才会在方法之前执行此操作。如果在此之前运行的某个没有启用@backupStaticAttributes
的测试方法改变了静态属性的值,那么被备份及还原的将会是这个改变后的值同样提供了黑名单支持:
class MyTest extends TestCase { protected $backupStaticAttributesBlacklist = [ 'className' => ['attributeName'] ]; // ... }
依赖关系
使用 @depends
声明测试方法所依赖的其他测试方法。 依赖方法的返回值,会作为被依赖方法的参数,其顺序和 @depends
的顺序一致,但是不会影响代码的执行顺序。
public function testOne()
{
$this->assertTrue(true);
return "depends1";
}
public function testTwo()
{
$this->assertTrue(true);
return "depends2";
}
/**
* 参数顺序对应 @depends 的顺序
* @depends testOne
* @depends testTwo
*/
public function testDepends()
{
$this->assertEquals(['depends1', 'depends2'], func_get_args());
}
测试结果:
... 3 / 3 (100%)
Time: 573 ms, Memory: 4.00MB
OK (3 tests, 3 assertions)
注意:
- 当被依赖的测试方法失败时,不会再执行依赖方法的测试。
- 如果被依赖方法返回的是对象,默认是引用传递,如果希望传递对象的副本时,使用:
@depends clone
数据供给器
使用 @dataProvider
声明数据供给器。 对应的方法需要返回:
- 数组(每个元素也是数组)
- 可遍历的对象(实现了迭代接口)
然后测试时,会将每次迭代器提供的一组数据进行测试,直到全部遍历完毕。
示例:
/**
* @dataProvider addtionProvider
*/
public function testSum($a, $b, $res)
{
$this->assertEquals($res, $a + $b);
}
public function addtionProvider()
{
return [
[1,3,4],
[1,1,2],
[1,1,3],
];
}
测试结果:
PHPUnit 7.5.1 by Sebastian Bergmann and contributors.
..F 3 / 3 (100%)
Time: 622 ms, Memory: 4.00MB
There was 1 failure:
1) TestDepends::testSum with data set #2 (1, 1, 3)
Failed asserting that 2 matches expected 3.
/vagrant/www/myyphp/index.php:16
FAILURES!
Tests: 3, Assertions: 3, Failures: 1.
注意点:
-
和 @depends 同时使用时,@provider 提供的参数会优先于 @depends 提供的参数,并且,依赖关系提供的参数不会变化。
public function testOne() { $this->assertTrue(true); return 'Depends1'; } /** * @depends testOne * @dataProvider addtionProvider */ public function testSum() { $this->assertEquals(['Provider1','Depends1'], func_get_args()); } //会测试两次,第一此传递:Provider1,第二次传递:Provider2 public function addtionProvider() { return [ ['Provider1'], ['Provider2'], ]; }
结果:
PHPUnit 7.5.1 by Sebastian Bergmann and contributors. ..F 3 / 3 (100%) Time: 644 ms, Memory: 4.00MB There was 1 failure: 1) TestDepends::testSum with data set #1 ('Provider2') Failed asserting that two arrays are equal. --- Expected +++ Actual @@ @@ Array ( - 0 => 'Provider1' + 0 => 'Provider2' 1 => 'Depends1' ) /vagrant/www/myyphp/index.php:23 FAILURES! Tests: 3, Assertions: 3, Failures: 1.
- 如果一个测试依赖于另外一个使用了数据供给器的测试,仅当被依赖的测试至少能在一组数据上成功时,依赖于它的测试才会运行
```php
/**
- @dataProvider addtionProvider */ public function testSum($a, $b, $res) { $this->assertEquals($res, $a + $b); return “depends”; }
/**
- 由于依赖的测试方法,全部都未通过测试,此时不会执行这个测试
- @depends testSum */ public function testOne() { $this->assertTrue(true); }
public function addtionProvider() { return [ [1,3,0], [1,1,0], ]; }
此时,数据供给器提供的数据全部无法通过测试,此时 `testOne` 测试方法不会被执行,会跳过: ```php PHPUnit 7.5.1 by Sebastian Bergmann and contributors. FFS 3 / 3 (100%) Time: 654 ms, Memory: 4.00MB There were 2 failures: 1) TestDepends::testSum with data set #0 (1, 3, 0) Failed asserting that 4 matches expected 0. /vagrant/www/myyphp/index.php:16 2) TestDepends::testSum with data set #1 (1, 1, 0) Failed asserting that 2 matches expected 0. /vagrant/www/myyphp/index.php:16 FAILURES! Tests: 3, Assertions: 2, Failures: 2, Skipped: 1.
- 使用了数据供给器的测试,其运行结果是无法注入到依赖于此测试的其他测试中的。也就是说,不能通过 return 返回值传到依赖于它的方法。
```php
/**
- @dataProvider addtionProvider */ public function testSum($a, $b, $res) { $this->assertEquals($res, $a + $b); return ‘hi’; }
/**
- @depends testSum */ public function testOne() { $this->assertEquals([‘hi’], func_get_args()); }
public function addtionProvider() { return [ [1,3,4], ]; }
无法接收到参数: ```php .F 2 / 2 (100%) Time: 616 ms, Memory: 4.00MB There was 1 failure: 1) TestDepends::testOne Failed asserting that two arrays are equal. --- Expected +++ Actual @@ @@ Array ( - 0 => 'hi' + 0 => null ) /vagrant/www/myyphp/index.php:26 FAILURES! Tests: 2, Assertions: 2, Failures: 1.
- 所有的数据供给器方法的执行都是在对
setUpBeforeClass
静态方法的调用和第一次对setUp
方法的调用之前完成的。因此,无法在数据供给器中使用创建于这两个方法内的变量。
异常测试
异常测试有两种方式:
- 在代码中使用:
$this->expectException(InvalidArgumentException::class);
- 使用标注:
@expectException
断言方法/标注:
- expectException
- expectExceptionCode
- expectExceptionMessage
- expectExceptionMessageRegExp
public function testException1()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage("hello");
throw new Exception('hello');
}
/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage hi
*/
public function testException2()
{
throw new InvalidArgumentException('hello');
}
运行结果:
FF 2 / 2 (100%)
Time: 171 ms, Memory: 10.00MB
There were 2 failures:
1) TestDemo::testException1
Failed asserting that exception of type "Exception" matches expected exception "InvalidArgumentException". Message was: "hello" at
/vagrant/www/myyphp/index.php:15
.
2) TestDemo::testException2
Failed asserting that exception message 'hello' contains 'hi'.
FAILURES!
Tests: 2, Assertions: 3, Failures: 2.
注意:不允许对 :Exception
类进行测试,异常类越明确越好。
错误测试
默认情况下,在测试过程中如果触发到了 PHP 的错误/警告,PHPUnit 会将其转换为异常:
PHPUnit\Framework\Error\Error\Notice
PHPUnit\Framework\Error\Error\Warning
PHPUnit\Framework\Error\Error\Error
public function testError()
{
$this->expectException(PHPUnit\Framework\Error\Error::class);
//此时触发一个错误
include 'file_not_existing_file.php';
}
结果:
. 1 / 1 (100%)
Time: 163 ms, Memory: 10.00MB
OK (1 test, 1 assertion)
注意:如果测试依靠会触发错误的 PHP 函数,例如 fopen
,我们可以通过抑制住错误通知,就能对返回值进行检查,否则错误通知将会导致抛出异常:
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;
}
// ...
}
}
输出内容测试
有时候,想要断言 某方法的运行过程中生成了预期的输出(例如,通过 echo 或 print)。PHPUnit\Framework\TestCase
类使用 PHP 的 输出缓冲 特性来为此提供必要的功能支持。
public function testOutput1()
{
$this->expectOutputString("hello");
echo "hello";
}
public function testOutput2()
{
//正则匹配
$this->expectOutputRegex("/\d+/");
print "hello world";
}
运行结果:
.F 2 / 2 (100%)
Time: 169 ms, Memory: 10.00MB
There was 1 failure:
1) TestDemo::testOutput2
Failed asserting that 'hello world' matches PCRE pattern "/\d+/".
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
用于输出内容测试的方法:
方法 | 含义 |
---|---|
void expectOutputRegex(string $regularExpression) |
设置输出预期为输出应当匹配正则表达式。 |
void expectOutputString(string $expectedString) |
设置输出预期为输出应当与 $expectedString 字符串相等。 |
bool setOutputCallback(callable $callback) |
设置回调函数,用来做诸如将实际输出规范化之类的动作。 |
string getActualOutput() |
获取实际输出。 |
==注意:在严格模式下,本身产生输出的测试将会失败。==
标记未完成 与 跳过
-
标记未完成
public function testMark() { $this->assertTrue(true); //在这里停止 $this->markTestIncomplete('后续还未完成'); }
执行结果:
I 1 / 1 (100%) Time: 253 ms, Memory: 4.00MB OK, but incomplete, skipped, or risky tests! Tests: 1, Assertions: 1, Incomplete: 1.
-
跳过测试
public function setUp() { if (!extension_loaded('mysqli')) { $this->markTestSkipped('MySQLi 扩展不可用'); } } public function testMark() { $this->assertTrue(true); }
测试结果:
. 1 / 1 (100%) Time: 239 ms, Memory: 4.00MB OK (1 test, 1 assertion)
还可以使用
@requires
来标识条件,如果条件不满足,也会跳过:/** * @requires PHP 7.3.0 */ public function testMark() { $this->assertTrue(true); }
其他条件: 类型| 可能的值| 范例| 其他范例 —|—|—|—|— PHP| 任何 PHP 版本标识符 |@requires PHP 5.3.3| @requires PHP 7.1-dev PHPUnit| 任何 PHPUnit 版本标识符 |@requires PHPUnit 3.6.3| @requires PHPUnit 4.6 OS| 用来对 PHP_OS 进行匹配的正则表达式| @requires OS Linux| @requires OS WIN32|WINNT function| 任何对 function_exists 而言有效的参数 |@requires function imap_open| @requires function ReflectionMethod::setAccessible extension| 任何扩展模块名,可以附带有版本标识符 |@requires extension mysqli | @requires extension redis 2.2.0
组织测试
PHPUnit 支持多种将多个测试组织在一起,形成组合测试套件。
- 文件系统方式
- 缺点:无法控制测试执行的顺序
- 用 XML 配置方式
文件系统方式
把所有测试用例源文件放在一个测试目录中,PHPUnit 会自动发现并运行测试 *Test.php
文件。
比如:
-vendor
-testes
Demo1Test.php
Demo1Test.php
Demo1Test.php
执行:
./vendor/bin/phpunit ./testes
//或者:
./vendor/bin/phpunit --bootstrap ./vendor/autoload.php ./testes/
即会自动执行这个3个测试类文件中的测试方法。
如果想要对运行哪些测试有更细粒度的控制,可以使用 --filter
选项,示例:
/vendor/bin/phpunit --filter Demo1Test ./testes
### XML 配置方式
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.3/phpunit.xsd"
backupGlobals="true"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<!-- 测试套件标签 -->
<testsuites>
<!-- 单个测试套件 -->
<testsuite name="demo">
<!-- 指定测试目录下的全部文件 -->
<directory>testes</directory>
<!-- 指定测试文件 -->
<file>test1/Demo4Test.php</file>
<!-- php的版本大于等于5.3.0才将文件添加到套件中 -->
<file phpVersion="5.3.0" phpVersionOperator=">=">test1/Demo5Test.php</file>
</testsuite>
</testsuites>
</phpunit>
根标签中更多的配置项参见手册:http://www.phpunit.cn/manual/current/zh_cn/appendixes.configuration.html
中文手册:http://www.phpunit.cn/manual/6.5/zh_cn/index.html