初识单元测试
测试驱动开发(TDD)
测试驱动开发是敏捷软件开发的推荐做法。TDD 希望在编写代码之前先编写测试。这些测试提供了必须遵循预期功能的代码。保持测试领先于开发,永远不会有未被测试的代码。编写测试代码的数量和代码和质量是成正比的。例如下面的例子:
class UserTest extends \Codeception\Test\Unit
{
public function testValidation()
{
$user = new User();
$user->setName(null);
$this->assertFalse($user->validate(['username']));
$user->setName('toolooooongnaaaaaaameeee');
$this->assertFalse($user->validate(['username']));
$user->setName('davert');
$this->assertTrue($user->validate(['username']));
}
}
行为驱动开发(BDD)
行为驱动开发是在 TDD 的基础上发展而来的,它为开发人员和非开发人员提供了一种通用语言,用于描述正确的应用程序行为和模块行为。相对于代码,BDD 通常是使用故事转化为测试的过程。例如以下的例子:
class BasicCest
{
// test
public function tryToTest(\AcceptanceTester $I)
{
$I->amOnPage('/');
$I->click('Login');
$I->fillField('username', 'john');
$I->fillField('password', 'coltrane');
$I->click('Enter');
$I->see('Hello, John');
$I->seeInCurrentUrl('/account');
}
}
什么是可测试的代码
短小但也不太复杂的代码,完整的注释,以及松偶合。这些特性更让代码具有“可测试性”。利用可测试特性和测试工具,可以让代码更具有可测试性。
开始
创建第一个单元测试模型
Yii2 的基础模板和高级模板中已经默认包含了单元测试工具 Codeception,可使用 PHPUnit 对项目进行 TDD 测试和使用 Chrome 驱动进行 BDD 测试。以下谨以高级模板为例进行说明。在项目的根目录中执行以下命令 codeception 工具将生成单元测试模型。
Lunix:
vendor\bin\codecept -c common generate:test unit models/Custom
Windows:
vendor\bin\codecept.bat -c common generate:test unit models/Custom
单元测试工具安装在项目的 vendor/bin 目录下,参数-c 指定单元测试工具的配置文件所在的目录:common,命令的开关是 generate:test,创建的类型是 nuit(单元测试),测试模型的生成目录为(基于 common/testes/unit/)models/Custom,生成的测试模型文件为 CustomTest.php。
执行单元测试
助手生成的单元测试模型默认携带了一个单元测试的功能点,我们可以执行以下命令进行测试
vendor\bin\codecept.bat -c common run unit
认识单元测试模型
<?php namespace common\tests\models;
class CustomTest extends \Codeception\Test\Unit
{
/**
* @var \common\tests\UnitTester
*/
protected $tester;
protected function _before()
{
}
protected function _after()
{
}
// tests
public function testSomeFeature()
{
}
}
代码中包含有两_before 和_after 两人拦截器方法,和一个以 test 起头的测试方法。_before 拦截器会在执行单元测试之前被执行,我们可以在这里进行一些测试的初始化操作,如:给要测试的变量进行赋值。
第一段测试代码
<?php namespace common\tests\models;
class TesterTest extends \Codeception\Test\Unit
{
/**
* @var \common\tests\UnitTester
*/
protected $tester;
protected function _before()
{
$this->tester = [
'one' => 12345,
'two' => 67890,
];
}
protected function _after()
{
}
// tests
public function testSomeFeature()
{
}
public function testCheckValue()
{
expect('check array type', $this->tester)->isArray();
expect('check has key', $this->tester)->hasKey('one');
}
}
我们在_before 拦截器中为类变量 tester 赋值,并添加一个新的测试方法 testCheckValue。使用 expect(预期)方法测试需要测试的值,这里我们要测试的是$this->tester,isArray(),我们预期得到的结果是数组类型,hasKey(‘one’),我们预期的结果为数据中包含 one 这个键。我们测试一下看看结果:
两个方法前面的"+"号,代表方法测试通过。
OK (2 tests, 2 assertions)
最后一行的信息显示,本次单元测试中总共两个测试方法(以 test 开头的方法),和两个断言(调用 expect 方法的次数)
制造一个失败的测试
为了看一下测试失败的结果,我们对代码进行一下改造:
protected function _before()
{
$this->tester = [
'done' => 12345,
'two' => 67890,
];
}
把数组的 one 修改为 done,执行测试:
现在我们可以看到,testCheckValue 方法名前面的状态显示为一个"x"(错误),错误的测试点(expect)的说明为"check has key",错误的描述为"Failed asserting that an array has the key ‘one’."(数组是否包含键名为 one 的键的断言失败)。#1 处提示出错误的代码所在的行号以方便我们的定位。
结尾的提示:
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
测试方法两个,断言两个,错误的断言一个。
更多的预期方法
项目 | Value |
---|---|
equals | 等于 |
notEquals | 不等于 |
contains | 数组中包含值,或字符串中包含字符串 |
notContains | 以上一条相反 |
true | 布尔:真 |
false | 布尔:假 |
null | 等于 null |
notNull | 不等于 null |
isEmpty | 为空 |
notEmpty | 非空 |
hasKey | 数组中包含键 |
hasntKey | 数组中不包含键 |
isInstanceOf | 对象比对为真 |
isNotInstanceOf | 对象比对为假 |
hasAttribute | 对象是包含属性 |
notHasAttribute | 对象不包含属性 |
hasStaticAttribute | 对象是否包含静态属性 |
notHasStaticAttribute | 对象不包含静态属性 |
count | 数组元素等于 |
notCount | 数组的元素不等于 |
equalXMLStructure | 等于 XML 文件结构 |
exists | 文件存在 |
notExists | 文件不存在 |
equalsJsonFile | 等于 JSON 文件数据 |
equalsJsonString | 等于 JSON 数据 |
regExp | 正则比对 |
equalsFile | 等于文件的数据 |
notEqualsFile | 不等于文件的数据 |
equalsXmlFile | 等于 XML 文件数据 |
equalsXmlString | 等于 XML 数据 |
stringContainsString | 字符串包含 |
stringNotContainsString | 字符串不包含 |
stringContainsStringIgnoringCase | 字符串包含,忽略大小写 |
stringNotContainsStringIgnoringCase | 字符串不包含,忽略大小写 |
isArray | |
bool | |
float | |
int | |
numeric | |
object | |
resource | |
string | |
scalar | |
isCallable | 是可调用类型 |
notArray | |
notBool | |
notFloat | |
notInt | |
notNumeric | |
notObject | |
notResource | |
notString | |
notScalar | |
notCallable | |
equalsCanonicalizing | |
notEqualsCanonicalizing | |
equalsIgnoringCase | |
notEqualsIgnoringCase | |
equalsWithDelta | |
notEqualsWithDelta | |
notStartsWith | |
startsWith | |
notEndsWith | |
endsWith | |
notSame | |
same | |
notMatchesFormatFile | |
matchesFormatFile | |
notMatchesFormat | |
matchesFormat | |
containsOnly | |
notContainsOnly | |
containsOnlyInstancesOf | |
internalType | |
notInternalType | |
greaterThan | |
lessThan | |
greaterOrEquals | |
lessOrEquals |
使用替身进行依赖注入
当我们在测试某些方法需要返回指定的值时,如从数据库模型里查询数据返回时,我们并不需要真的去数据库里为这次测试插入一条测试数据。我们可以使用测试工具提供的机制,把某个类的指定的方法进行覆盖。
public function testIsAdmin()
{
$user = $this->make(\yii\web\User::class, [
'getIdentity' => function($autoRenew = true) {
return true;
}
]
);
expect('check is admin', $user->getIdentity())->true();
}
我们覆盖\yii\web\User 类中 getIdentity 方法的返回值并进行测试。我们可以控制方法的返回值,以便进行相关功能的测试。
提示:
使用依赖注入的对象,可以把我们替身对象注入到要测试的对象中进行后续的测试。如果是对象从内部执行 new 生成的 User 对象,我们就无法完成上面的测试,只能从数据库同添加相关的数据才能进行后续的测试。