2 编写PHPUnit测试
类测试
- 针对类的测试,命名习惯是
$className+Test
,即类名+Test
。 - 该类通常继承
PHPUnit\Framework\TestCase
- 测试方法都是以
test+$functionName
命名的,配合phpDocumentor
,可以在注释中使用@test
方法标注为测试方法 - 继承的
PHPUnit\Framework\TestCase
中有很多判断方法,这些方法都是以assert
开头的。
具体的函数名太多了,肯定记不住,就说他们的大致内容,当你需要的时候再去找对应的方法:
- 数据类型判断,
int
,float
,string
等 - 文件,路径等,该文件是否可读,路径是否存在,该文件是否存在等
- 数组判断,该数组中是否有该值
- 字符串判断,字符串是否以指定字符串开头,字符串是否包含指定字符
- xml文件判断,两个xml文件是否相等等
- 对象判断,该对象是否包含指定属性,两个对象是否相同,该对象是否是一个类的实例等
- 大于,小于等判断
以上判断有是否相等,就会有不想等的对应函数。
<?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));
}
}
?>
判断都是正确时,系统执行结果:
判断错误,系统执行结果:
测试依赖关系:@depends
PHPUnit支持对测试方法之间的显式依赖关系进行声明。这种依赖关系并不是定义在测试方法的执行顺序中,而是允许生产者(producer)返回一个测试基境(fixture)的实例,并将此实例传递给依赖于它的消费者(consumer)们。
- 生产者(producer),是能生成被测单元并将其作为返回值的测试方法。
- 消费者(consumer),是依赖于一个或多个生产者及其返回值的测试方法。
class UserStoreTest extends TestCase{
public function testDepends(){
$this->testPop(
$this->testPush(
$this->testEmpty()
)
);
}
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);
}
}
我感觉没有特定的生成者与消费者,而是看调用关系,比如testPush
依赖testEmpty
,则testEmpty
是生产者,而testPush
是消费者。再来看testPop
和testPush
,testPush
这时就是生产者,而testPop
则是消费者。
默认情况下,生产者所产生的返回值将“原样”传递给相应的消费者。这意味着,如果生产者返回的是一个对象,那么传递给消费者的将是一个指向此对象的引用。如果需要传递对象的副本而非引用,则应当用 @depends clone 替代 @depends。
class UserStoreTest extends TestCase{
public function testOne(){
$this->assertTrue(false);
}
/**
* @depends testOne
*/
public function testTwo(){
}
}
这里讲一些注意点,PHPUnit
会把你的类中的所有public
方法从上到下执行一遍,所以如果你要是有对应的依赖,则生产者要在消费者前面。
而且因为phpunit
会自动创建类的实例,所以你不需要在代码下面创建对应的实例并显示调用其中的方法,但是你要使用@depends
指定依赖:
class UserStoreTest 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()
{
$arguments=func_get_args();
print_r($arguments);
$this->assertEquals(
['first', 'second'],
$arguments
);
}
}
这里phpunit
会自动将
class UserStoreTest 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()
{
$arguments=func_get_args();
print_r($arguments);
$this->assertEquals(
['first', 'second'],
$arguments
);
}
}
这里phpunit
会自动将testProducerFirst
和testProducerSecond
的返回值作为参数传入testConsumer
中。
数据供给器:@dataProvider
顾名思义,测试一些类的时候需要提供数据,这里phpunit
可以设置一个方法,返回测试的数据,但是强制返回数组,或者可以循环的对象。
返回数组
class UserStoreTest extends TestCase{
/**
* @param $a
* @param $b
* @param $expected
* @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, 2]
];
}
}
重点是数组中的$value
,$key
只是用来做定位具体哪一行的,比如:
/**
* Class UserStoreTest
*/
class UserStoreTest extends TestCase{
/**
* @param $a
* @param $b
* @param $expected
* @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]
];
}
}
错误提示信息:s
1) UserStoreTest::testAdd with data set "one plus one" (1, 1, 3)
返回迭代器对象
创建迭代器类:implements \Iterator
这个其实是要让类继承一个接口:
class Row implements \Iterator{
private $array;
private $index=0;
public function __construct()
{
$this->array=array(
array(1,2,3),
array(3,4,5),
array(6,7,8)
);
$this->index=0;
}
/**
* 将指针指向下一个
*/
public function next()
{
print "next\r\n";
$this->index++;
}
/**
* 验证是否还需要继续循环
* @return bool|void
*/
public function valid()
{
print "valid\r\n";
return isset($this->array[$this->index]);
}
/**
* 获取当前的key
* @return mixed|void
*/
public function key()
{
print "key\r\n";
return $this->index;
}
/**
* 重制
*/
public function rewind()
{
print "rewind\r\n";
$this->index=0;
}
/**
* 获取当前值
* @return mixed
*/
public function current()
{
print "current\r\n";
return $this->array[$this->index];
}
}
$row=new Row();
foreach ($row as $key=>$value){
print $key." #################################################\r\n";
}
输出结果
rewind
valid
current
key
0 #################################################
next
valid
current
key
1 #################################################
next
valid
current
key
2 #################################################
next
valid
current
key
3 #################################################
next
valid
测试类
require_once __DIR__."/../../vendor/autoload.php";
require_once __DIR__."/../Row.php";
use PHPUnit\Framework\TestCase;
/**
* Class UserStoreTest
*/
class UserStoreTest extends TestCase{
/**
* @param $a
* @param $b
* @param $expected
* @dataProvider additionProvider
*/
public function testAdd($a,$b,$expected){
// 参数的数量是随意的,按照顺序从数据提供器中获取
}
public function additionProvider(){
// 这里返回的值一定要是类似二维数组的值,不论就是二维数组,还是迭代器嵌套迭代器
return new Row();
}
}
@depends
和@dataProvider
组合使用
class UserStoreTest 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()
{
$argument=func_get_args();
print "输出数据\r\n";
print_r($argument);
print "\r\n";
$this->assertEquals(
['provider1', 'first', 'second'],
$argument
);
}
}
系统会先将@dataProvider
作为参数,接着就是对应的@depends
,比如这里输出的数据就是:
输出数据
Array(
[0]=>"provider1",
[1]=>"first",
[2]=>"second"
)
输出数据
Array(
[0]=>"provider2",
[1]=>"first",
[2]=>"second"
)
注意事项
如果一个测试依赖于另外一个使用了数据供给器的测试,仅当被依赖的测试至少能在一组数据上成功时,依赖于它的测试才会运行。
使用了数据供给器的测试,其运行结果是无法注入到依赖于此测试的其他测试中的。
直接上代码可能比较好理解一点:
class UserStoreTest extends TestCase{
public function provider()
{
return [['provider3'], ['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()
{
$argument=func_get_args();
// 这里的两条数据都是错误的,所以不会运行到 testAfterDepends 中
$this->assertEquals(
['provider1', 'first', 'second'],
$argument
);
return $argument;
}
/**
* @depends testConsumer
*/
public function testAfterDepends(){
$arguments=func_get_args();
print "\r\n";
print "输出数据\r\n";
print_r($arguments);
}
}
再来
class UserStoreTest 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()
{
$argument=func_get_args();
$this->assertEquals(
['provider1', 'first', 'second'],
$argument
);
return $argument;
}
/**
* @depends testConsumer
*/
public function testAfterDepends(){
// 第一条数据是正确的,但是 testConsumer 的返回值是没有办法传递到 testAfterDepends 中的
$arguments=func_get_args();
print "\r\n";
print "输出数据\r\n";
print_r($arguments);
}
}
所有的数据供给器方法的执行都是在对
setUpBeforeClass
静态方法的调用和第一次对setUp
方法的调用之前完成的。因此,无法在数据供给器中使用创建于这两个方法内的变量。这是必须的,这样 PHPUnit 才能计算测试的总数量。
暂时没有讲到这两个函数,所以暂时先不用管。
对异常进行测试
所谓对异常进行测试,就是测试在以下代码中会不会出现异常,比如在下面的代码中,只有抛出异常,系统才是正常执行的,否则就是异常的。这里的例子演示了2种异常测试的方式,方式1,$this->expectException
,另一种就是@expectedException Exception
,但是第二种不建议使用。
class UserStoreTest extends TestCase{
public function testException(){
// 指定捕获的异常
$this->expectException(\Exception::class);
$this->result(true);
$this->result(false);
}
/**
* 不建议使用,因为已经快要被取消了
* @expectedException Exception
* @throws Exception
*/
public function testExceptionUseDoc(){
$this->result(true);
$this->result(false);
}
protected function result($result){
if(!$result){
throw new \Exception("抛出异常");
}else{
return $result;
}
}
}
不止可以对异常的种类进行限制,还可以对异常的数字,抛出异常的文字,以及针对对应文字进行正则判断等:
- expectExceptionCode
- expectExceptionMessage
- expectExceptionMessageRegExp
对PHP错误进行测试
默认情况下,PHPUnit 将测试在执行中触发的 PHP 错误、警告、通知都转换为异常。
所以你只需要抓捕异常。
class UserStoreTest extends TestCase{
public function testFailingInclude()
{
$this->expectException("PHPUnit\Framework\Error\Error");
include 'not_existing_file.php';
}
}
对异常进行测试是越明确越好的。对太笼统的类进行测试有可能导致不良副作用。因此,不再允许用
@expectedException
或setExpectedException()
对Exception
类进行测试。
对输出进行测试
简单来说就是对print
,echo
等值的输出进行判断
class UserStoreTest extends TestCase{
public function testExpectFooActualFoo()
{
$this->expectOutputString('foo');
print 'foo';
}
public function testExpectBarActualBaz()
{
$this->expectOutputString('bar');
print 'baz';
}
}