一、概述
1. 什么是单元测试?
【百度百科】单元测试是对软件中的最小可测单元进行检查和验证。
是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。
2. 作用是什么?
【废话】检查软件、程序的可行性,稳定性。
通过单元测试能够避免在迭代、升级等过程中,引起重复的、多余的问题。
避免在别人修改代码的时候,影响到你的逻辑
3. 哪些程序需要写单元测试(PHP)?
【理想】理想的单元测试应当覆盖程序中所有可能的路径,包括正确的和错误的路径,个单元测试通常覆盖一个函数或方法中的一个特定路径。
【现实】model、helper、controller中的函数必须测试、路径覆盖到所有可能性
二、PHPUnit的安装与集成CI框架
略。。。。。后续再补
三、PHPUnit的使用
编写测试用例
测试的依赖关系 @depends
PHPUnit支持对测试方法之间的显式依赖关系进行声明。这种依赖关系并不是定义在测试方法的执行顺序中,而是允许生产者(producer)返回一个测试基境(fixture)的实例,并将此实例传递给依赖于它的消费者(consumer)们。
class StackTest extends PHPUnit_Framework_TestCase
{
public function testEmpty()
{
$stack = array();
$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);
}
}
?>
默认情况下,生产者所产生的返回值将“原样”传递给相应的消费者。这意味着,如果生产者返回的是一个对象,那么传递给消费者的将是一个指向此对象的引用。如果需要传递对象的副本而非引用,则应当用 @depends clone 替代 @depends。
数据供给器 @dataProvider
测试方法可以接受任意参数。用 @dataProvider 标注来指定使用哪个数据供给器方法。
数据供给器方法必须声明为 public,其返回值要么是一个数组,其每个元素也是数组;要么是一个实现了 Iterator 接口的对象,在对它进行迭代时每步产生一个数组。每个数组都是测试数据集的一部分,将以它的内容作为参数来调用测试方法。
class DataTest extends PHPUnit_Framework_TestCase
{
/**
@dataProvider additionProvider
*/
public function testAdd($a, $b, $expected)
{
$this->assertEquals($expected, $a + $b);
}
public function additionProvider()
{
return array(
array(0, 0, 0),
array(0, 1, 1),
array(1, 0, 1),
array(1, 1, 3)
);
}
}
?>
也可以是这样:
class DataTest extends PHPUnit_Framework_TestCase
{
/**
* @dataProvider additionProvider
*/
public function testAdd($a, $b, $expected)
{
$this->assertEquals($expected, $a + $b);
}
public function additionProvider()
{
return array(
'adding zeros' => array(0, 0, 0),
'zero plus one' => array(0, 1, 1),
'one plus zero' => array(1, 0, 1),
'one plus one' => array(1, 1, 3)
);
}
}
?>
对PHP错误进行测试
默认情况下,PHPUnit 将测试在执行中触发的 PHP 错误、警告、通知都转换为异常。利用这些异常,就可以,比如说,预期测试将触发 PHP 错误
class ExpectedErrorTest extends PHPUnit_Framework_TestCase
{
/**
@expectedException PHPUnit_Framework_Error
*/
public function testFailingInclude()
{
include 'not_existing_file.php';
}
}
?>
测试
phpunit -d error_reporting=2 ExpectedErrorTest
PHPUnit 5.2.0 by Sebastian Bergmann and contributors.
.
Time: 0 seconds, Memory: 5.25Mb
OK (1 test, 1 assertion)
命令行测试执行器
说明
PHPUnit 测试执行器可通过phpunit 调用,例如在CI中:
tongkundeMacBook-Pro:www tongkun$ cd tests/
tongkundeMacBook-Pro:tests tongkun$ phpunit
PHPUnit 5.0.0 by Sebastian Bergmann and contributors.
............. 13 / 13 (100%)
Time: 195 ms, Memory: 17.50Mb
OK (13 tests, 8 assertions)
说明:
先进入测试的根目录,执行phpunit 命令,后面可跟具体的目录或文件,也可不跟,如果没有则会对当前目录的所有文件执行单元测试,对于每个测试的运行,PHPUnit命令行工具会输出一个字符来指示进展:
. 当测试陈宫时输出
F 当测试方法运行过程中一个断言失败时输出,例如一个失败的assertEquals()调用
E 当测试方法运行过程中产生一个错误时输出,错误是指意料之外的异常(exception)或者PHP错误
R 当测试被标记有风险时输出
S 当测试跳出时输出
I 当测试被标记不完整或为实现时输出
常用命令行选项
--coverage-clover:为运行的测试生成带有代码覆盖率信息的 XML 格式的日志文件
--coverage-html:生成 HTML 格式的代码覆盖率报告
--coverage-php:生成一个序列化后的 PHP_CodeCoverage 对象,此对象含有代码覆盖率信息
--log-json:生成 JSON 格式的日志文件
--filter:只运行名称与给定模式匹配的测试。如果模式未闭合包裹于分隔符,PHPUnit 将用 / 分隔符对其进行闭合包裹
tongkundeMacBook-Pro:tests tongkun$ phpunit --filter 'WelcomeTest::testTest'
PHPUnit 5.0.0 by Sebastian Bergmann and contributors.
... 3 / 3 (100%)
Time: 148 ms, Memory: 16.75Mb
OK (3 tests, 3 assertions)
这样测试的就是WelcomeTest类中的testTest函数,过滤模式的例子有很多,详见文档官方
--colors:使用彩色输出。Windows下,用 ANSICON 或 ConEmu。
本选项有三个可能的值:
never: 完全不使用彩色输出。当未使用 --colors 选项时,这是默认值。
auto: 如果当前终端不支持彩色、或者输出被管道输出至其他命令、或输出被重定向至文件时,不使用彩色输出,其余情况使用彩色。
always: 总是使用彩色输出,即使当前终端不支持彩色、输出被管道输出至其他命令、或输出被重定向至文件。
当使用了 --colors 选项但未指定任何值时,将选择 auto 做为其值。
--stop-on-error:首次错误出现后停止执行。
--stop-on-failure:首次错误或失败出现后停止执行。
--stop-on-risky:首次碰到有风险的测试时停止执行。
--stop-on-risky:首次碰到有风险的测试时停止执行。
--stop-on-incomplete首次碰到不完整的测试时停止执行。
--repeat:将测试重复运行指定次数。
tongkundeMacBook-Pro:tests tongkun$ phpunit --repeat 10
PHPUnit 5.0.0 by Sebastian Bergmann and contributors.
............................................................... 63 / 130 ( 48%)
............................................................... 126 / 130 ( 96%)
.... 130 / 130 (100%)
Time: 456 ms, Memory: 26.25Mb
OK (130 tests, 80 assertions)
--tap:使用 Test Anything Protocol (TAP) 报告测试进度
tongkundeMacBook-Pro:tests tongkun$ phpunit --tap
TAP version 13
ok 1 - CI_Unit_Test_class_Test::test_CI_Unit_Test_Class
ok 2 - SomeControllerTest::testWelcomeController
ok 3 - WelcomeTest::testIndex
ok 4 - WelcomeTest::testTest
ok 5 - WelcomeTest::testOutput
ok 6 - WelcomeTest::testTest1
ok 7 - WelcomeTest::testTest2
ok 8 - HelperTest::testSampleFunction
ok 9 - SomeLibTest::testMethod
ok 10 - M_user_masterTest::testSelect
ok 11 - M_user_masterTest::testInsert
ok 12 - PHPTest::testFunctionJsonEncode
ok 13 - PHPTest::testPhpVersion
1..13
--configuration, -c:从 XML 文件中读取配置信息。更多细节请参见附录 C。
如果 phpunit.xml 或 phpunit.xml.dist (按此顺序)存在于当前工作目录并且未使用 --configuration,将自动从此文件中读取配置。
tongkundeMacBook-Pro:tests tongkun$ phpunit --configuration phpunit.xml
PHPUnit 5.0.0 by Sebastian Bergmann and contributors.
............. 13 / 13 (100%)
Time: 209 ms, Memory: 17.50Mb
OK (13 tests, 8 assertions)
--no-configuration:忽略当前工作目录下的 phpunit.xml 与 phpunit.xml.dist。
四、基境(fixture)
什么是基境?
“基境”就是编写代码来将整个场景设置成某个已知的状态,并在测试结束后将其复原到初始状态。这个已知的状态称为测试的 基境(fixture)。
基境的建立
PHPUnit 支持共享建立基境的代码。在运行某个测试方法前,会调用一个名叫 setUp() 的模板方法。setUp() 是创建测试所用对象的地方。当测试方法运行结束后,不管是成功还是失败,都会调用另外一个名叫 tearDown() 的模板方法。tearDown() 是清理测试所用对象的地方。
class StackTest extends PHPUnit_Framework_TestCase
{
protected $stack;
protected function setUp()
{
$this->stack = array();
}
public function testEmpty()
{
$this->assertTrue(empty($this->stack));
}
public function testPush()
{
array_push($this->stack, 'foo');
$this->assertEquals('foo', $this->stack[count($this->stack)-1]);
$this->assertFalse(empty($this->stack));
}
public function testPop()
{
array_push($this->stack, 'foo');
$this->assertEquals('foo', array_pop($this->stack));
$this->assertTrue(empty($this->stack));
}
}
?>
测试类的每一个方法都会运行一次setUp()和tearDown()模板方法(同时,每个测试方法都在一个全新的测试类实例上运行),
另外,setUpBeforeClass() 与 tearDownAfterClass() 模板方法将分别在测试用例类的第一个测试运行之前和测试用例类的最后一个测试运行之后调用。基境共享可以在共享数据库连接时使用;
五、组织测试
用文件系统来编排测试套件
例如:
phpunit controllers/WelcomeTest.php
用 XML 配置来编排测试套件
//配置文件
colors="true" //颜色
stopOnFailure="false" //出错后是否终止
bootstrap="../application/third_party/CIUnit/bootstrap_phpunit.php"> //bootstrap 地址
//测试套件
controllers //要测试的目录
helpers
libs
models
system
vendor
libs
controllers
fixtures
models
helpers
六、有风险的测试
无用测试
PHPUnit 可以更严格对待事实上不测试任何内容的测试。此项检查可以用命令行选项 --report-useless-tests 或在 PHPUnit 的 XML 配置文件中设置 beStrictAboutTestsThatDoNotTestAnything="true" 来启用。
在启用本项检查后,如果某个测试未进行任何断言,它将被标记为有风险。仿件对象中的预期和诸如 @expectedException 这样的标注同样视为断言。
测试执行期间产生的输出
PHPUnit 可以更严格对待测试执行期间产生的输出。 此项检查可以用命令行选项 --disallow-test-output 或在 PHPUnit 的 XML 配置文件中设置 beStrictAboutOutputDuringTests="true" 来启用。
在启用本项检查后,如果某个测试产生了输出,例如,在测试代码或被测代码中调用了 print,它将被标记为有风险。
七、未完成的测试与跳过的测试
未完成的测试
开始写新的测试用例类时,可能想从写下空测试方法开始,比如:
public function testSomething()
{
}
假如把成功的测试视为绿灯、测试失败视为红灯,那么还额外需要黄灯来将测试标记为未完成或尚未实现。PHPUnit_Framework_IncompleteTest 是一个标记接口,用于将测试方法抛出的异常标记为测试未完成或目前尚未实现而导致的结果。PHPUnit_Framework_IncompleteTestError 是这个接口的标准实现。
例如:我们有一个测试文件,contrllers/WelcomeTest.php,其中有一个测试方法,通过在测试方法中调用markTestIncomplete()将这个测试标记为未完成。
public function testTest() {
$this->assertTrue(true,'这里可以正常工作');
$this->markTestIncomplete('此测试尚未实现');
}
在PHPUnit命令行测试执行器中输出,未完成的测试标记为1, 如下:
localhost:tests tongkun$ phpunit
PHPUnit 5.0.0 by Sebastian Bergmann and contributors.
R..I...RRRR.. 13 / 13 (100%)
Time: 187 ms, Memory: 17.75Mb
OK, but incomplete, skipped, or risky tests!
Tests: 13, Assertions: 8, Incomplete: 1, Risky: 5.
跳过测试
如上,如果有些测试需要某些环境或者配置才能完成,则可选择跳过,通过调用 markTestSkipped() 方法来测试
用 @requires 来跳过测试
除了上述方法,还可以用 @requires 标注来表达测试用例的一些常见前提条件。
事例:
例 7.3: 用 @requires 来跳过测试
/**
* @requires extension mysqli
*/
class DatabaseTest extends PHPUnit_Framework_TestCase
{
/**
* @requires PHP 5.3
*/
public function testConnection()
{
// 测试要求有 mysqli 扩展,并且 PHP >= 5.3
}
// ... 所有其他要求有 mysqli 扩展的测试
}
?>
要求安装mysqli苦战和php 5.3 才能执行
#常用断言
前边废话一篇,终于到了关键的断言部分,断言可以说是单元测试的核心,通过断言的校验,保证程序的正确运行,并输出正确的值。
assertArrayHasKey()
assertArrayHasKey(mixed $key, array $array[, string $message = ''])
当 $array 不包含 $key 时报告错误,错误讯息由 $message 指定。
assertArrayNotHasKey() 是与之相反的断言,接受相同的参数。
assertContains()
assertContains(mixed $needle, Iterator|array $haystack[, string $message = ''])
当 $needle 不是 $haystack的元素时报告错误,错误讯息由 $message 指定。
assertNotContains() 是与之相反的断言,接受相同的参数。
assertContains(string $needle, string $haystack[, string $message = '', boolean $ignoreCase = FALSE])
当 $needle 不是 $haystack 的子字符串时报告错误,错误讯息由 $message 指定。
assertContainsOnly()
assertContainsOnly(string $type, Iterator|array $haystack[, boolean $isNativeType = NULL, string $message = ''])
当 $haystack 并非仅包含类型为 $type 的变量时报告错误,错误讯息由 $message 指定。
$isNativeType 是一个标志,用来表明 $type 是否是原生 PHP 类型。
assertEmpty()
assertEmpty(mixed $actual[, string $message = ''])
当 $actual 非空时报告错误,错误讯息由 $message 指定。
assertNotEmpty() 是与之相反的断言,接受相同的参数。
assertAttributeEmpty() 和 assertAttributeNotEmpty() 是便捷包装(convenience wrapper),可以应用于某个类或对象的某个 public、protected 或 private 属性。
assertEquals()
assertEquals(mixed $expected, mixed $actual[, string $message = ''])
当两个变量 $expected 和 $actual 不相等时报告错误,错误讯息由 $message 指定。
assertNotEquals() 是与之相反的断言,接受相同的参数。
注意特定类型的比较(浮点型等),详见文档
assertFalse()
assertFalse(bool $condition[, string $message = ''])
当 $condition 为 TRUE 时报告错误,错误讯息由 $message 指定。
assertNotFalse() 是与之相反的断言,接受相同的参数。
assertNull()
assertNull(mixed $variable[, string $message = ''])
当 $actual 不是 NULL 时报告错误,错误讯息由 $message 指定。
assertNotNull() 是与之相反的断言,接受相同的参数。
assertRegExp()
assertRegExp(string $pattern, string $string[, string $message = ''])
当 $string 不匹配于正则表达式 $pattern 时报告错误,错误讯息由 $message 指定。
assertNotRegExp() 是与之相反的断言,接受相同的参数。
assertStringMatchesFormat()
assertStringMatchesFormat(string $format, string $string[, string $message = ''])
当 $string 不匹配于 $format 定义的格式时报告错误,错误讯息由 $message 指定。
assertStringNotMatchesFormat() 是与之相反的断言,接受相同的参数。
assertSame()
assertSame(mixed $expected, mixed $actual[, string $message = ''])
当两个变量 $expected 和 $actual 的值与类型不完全相同时报告错误,错误讯息由 $message 指定。
assertNotSame() 是与之相反的断言,接受相同的参数。
assertAttributeSame() 和 assertAttributeNotSame() 是便捷包装(convenience wrapper),以某个类或对象的某个 public、protected 或 private 属性作为实际值来进行比较。
assertTrue()
assertTrue(bool $condition[, string $message = ''])
当 $condition 为 FALSE 时报告错误,错误讯息由 $message 指定。
assertNotTrue() 是与之相反的断言,接受相同的参数。