2. 继续 Hello World
上一章的 Hello World 程序,我们只验证了用户输入的 username 不为空,在真实的项目里,这显然是不够的。
本章让我们通过完善 username 的验证来继续学习 PHPUnit 。
2.1 测试异常
假设我们要求 username 的长度要在 2 ~ 30 个字符之间,并且只能包含英文大小写字母、数字、空格,其它的字符均不允许出现。
/var/www/ourblog/hello-world/func.php
1 <?php
2
3 function sayHello(array $data)
4 {
5 if (isset($data['username']) && $data['username']) {
6 if (!preg_match('/^[a-zA-z0-9 ]{2,30}$/', $data['username'])) {
7 throw new InvalidArgumentException('invalid username, should 2 ~ 30 char\
8 acters and only contains a-z, A-Z, 0-9 and space.');
9 }
10 return 'Hello ' . $data['username'];
11 }
12
13 return 'Hello World';
14 }
PHPUnit 提供了 expectException() 方法来测试异常。
新建文件 /var/www/hello-world/ExceptionTest.php
1 <?php
2
3 class ExceptionTest extends PHPUnit\Framework\TestCase
4 {
5 public function testTooShortUsername()
6 {
7 $this->expectException(InvalidArgumentException::class);
8 $this->expectExceptionMessage('invalid username, should 2 ~ 30 characters an\
9 d only contains a-z, A-Z, 0-9 and space.');
10
11 $data = array('username' => 'a');
12 sayHello($data);
13 }
14
15 public function testTooLongUsername()
16 {
17 $this->expectException(InvalidArgumentException::class);
18 $this->expectExceptionMessage('invalid username, should 2 ~ 30 characters an\
19 d only contains a-z, A-Z, 0-9 and space.');
20
21 $data = array('username' => str_pad('a', 31, 'A'));
22 sayHello($data);
23 }
24
25 public function testUsernameContainsNotAllowedChars()
26 {
27 $this->expectException(InvalidArgumentException::class);
28 $this->expectExceptionMessage('invalid username, should 2 ~ 30 characters an\
29 d only contains a-z, A-Z, 0-9 and space.');
30
31 $data = array('username' => 'Bob!');
32 sayHello($data);
33 }
34 }
运行 phpunit
上一章我们写了3个测试用例,每个测试用例里有1个 assert ,这次的这个 ExceptionTest 里我们又写了3个测试用例,每个测试用例里有2个 expect ,加起来一共 6 tests, 9 assertions 。
读者也许意识到了,我们新写的这3个测试用例除了输入参数不一样,其它的代码都是一样的。那么有没有办法简化测试代码呢?
2.2 @dataProvider
PHPUnit 提供了 @dataProvider 注释来简化上一小节我们遇到的情况。
新建文件 /var/www/hello-world/DataProviderTest.php
1 <?php
2
3 class DataProviderTest extends PHPUnit\Framework\TestCase
4 {
5 /**
6 * @dataProvider invalidUsernameProvider
7 */
8 public function testInvalidUsername($invalidUsername)
9 {
10 $this->expectException(InvalidArgumentException::class);
11 $this->expectExceptionMessage('invalid username, should 2 ~ 30 characters an\
12 d only contains a-z, A-Z, 0-9 and space.');
13
14 $data = array('username' => $invalidUsername);
15 sayHello($data);
16 }
17
18 public function invalidUsernameProvider()
19 {
20 return array(
21 array('a'),
22 array(str_pad('a', 31, 'A')),
23 array('Bob!')
24 );
25 }
26 }
运行 phpunit
现在我们有了 9 tests, 15 assertions 。
回过头来再看下 sayHelloTest ,读者应该意识到那3个测试用例也可以使用 @dataProvider 简化。
新建文件 /var/www/hello-world/sayHelloWithDataProviderTest.php
1 <?php
2
3 class sayHelloWithDataProviderTest extends PHPUnit\Framework\TestCase
4 {
5 /**
6 * @dataProvider validUsernameProvider
7 */
8 public function testEmptyInput($data, $ret)
9 {
10 $this->assertEquals($ret, sayHello($data));
11 }
12
13 public function validUsernameProvider()
14 {
15 return array(
16 array(array(), 'Hello World'),
17 array(array('username' => ''), 'Hello World'),
18 array(array('username' => 'Bob'), 'Hello Bob')
19 );
20 }
21 }
运行 phpunit
现在我们有了 12 tests, 18 assertions 。
2.3 @group
到上一小节结尾,我们一共写了 12 个测试用例,每次执行 phpunit 时,这 12 个测试用例都会跑一遍。在一个稍大点的项目里,会有成百上千的测试用例,全部运行下来会花点时间,也许几十秒,也许几分钟。
有没有办法只运行当前正在开发的功能块的测试,而不运行其它的呢?
PHPUnit 提供了 @group 注释来达到这一目的。
作者特别提示:
如果所有测试跑的足够快,能在几秒内结束,就没必要使用 @group 来单独跑某几个测试。毕竟自动化测试的一个很重要的目的就是回归测试,确保新的修改不会影响到原来的功能。
不管所有测试跑的快慢,即使开发者可以在开发过程中使用 @group ,但在将新的修改提交到代码库之前,也应该完整的跑一次测试,确保新的修改没有影响到已有的功能。
我们给 ExceptionTest 加上 @group exception 注释, 给 DataProviderTest 、sayHelloWithDataProviderTest 加上 @group dataProvider 注释。
1 <?php
2 /**
3 * @group exception
4 */
5 class ExceptionTest extends PHPUnit\Framework\TestCase
6 {
7 // ...
8 }
9 <?php
10 /**
11 * @group dataProvider
12 */
13 class DataProviderTest extends PHPUnit\Framework\TestCase
14 {
15 // ...
16 }
17 <?php
18 /**
19 * @group dataProvider
20 */
21 class sayHelloWithDataProviderTest extends PHPUnit\Framework\TestCase
22 {
23 // ...
24 }
然后分别运行它们。
2.4 Code Coverage
PHPUnit 结合 php-xdebug 扩展可以提供代码测试覆盖率报告。
首先,我们要安装 php-xdebug 。
1 bob@Bob-VirtualBox:~$ sudo apt install php-xdebug
然后修改 phpunit.xml 告诉 phpunit 我们要生成代码测试覆盖率报告。
1 <phpunit bootstrap="bootstrap.php" stopOnFailure="true" cacheResult="false">
2 <testsuites>
3 <testsuite name="All">
4 <directory>.</directory>
5 </testsuite>
6 </testsuites>
7 <filter>
8 <whitelist>
9 <file>func.php</file>
10 </whitelist>
11 </filter>
12 <logging>
13 <log type="coverage-html" target="report"/>
14 </logging>
15 </phpunit>
运行 phpunit
浏览器打开 /var/www/hello-world/report/index.html
鼠标放到代码上,还能看到这行代码都被哪几个测试用例覆盖到了。
2.5 关于 Code Coverage 的进一步思考
现在让我们仔细看一下验证 username 的正则表达式:
a-zA-z0-9再来回顾下我们写的测试用例,会发现:
- 非法字符我们仅测试了一个“!”号
- 有效字符空格我们没有测试
可是我们的测试覆盖率已经 100% 了!
甚至我们将非法字符的测试去掉,再去掉一个长度的测试后,覆盖率依旧是 100% !
这说明了什么?
Bob Test Theory 2
测试覆盖率是一个参考,是开发者的一个辅助工具,开发人员可以在开发过程中,利用测试覆盖率检查疏漏,补全测试。把测试覆盖率作为一个指标强行要求开发者达到高覆盖率,除了引起开发者的反感外,恐怕没别的好处。
PHPUnit 也提供了几种方法让一段代码不包含在覆盖率统计范围内,所以开发者想要达到高覆盖率是很容易的。那些妄图在覆盖率上打开发者主意的,就省省吧。
2.6 PHPUnit 手册
本书并不打算翻译或者精简重述 PHPUnit 手册,那叫 ”嚼剩饭“ 。
我们通过这个简单的 Hello World 程序已经介绍了大部分常用的 PHPUnit 功能。还有两个重要的功能 Fixtures 和 Mock 通过这个 Hello World 无法展示了,我们将其留到实战项目里。
PHPUnit 提供了很多方便的 assertion,读者请查阅 PHPUnit 手册: Assertions 大概过一遍,留个印象,在需要时再仔细翻看其用法。
如果读者有耐心,也可以翻阅下手册中的其它章节。PHPUnit 提供的功能很丰富,但在实际项目中,往往用不了那么多。
Bob Test Theory 3
我们一定不要为了测试而测试,不要为了 TDD 而 TDD 。
写测试、TDD 都是手段,目的是要解放开发者,让开发人员有更多的时间去思考,去写出更高质量、更易维护的代码。
2.7 本章小结
本章我们结束了 PHPUnit Hello World 程序的测试,介绍了如何测试异常,如何使用 @dataProvider 简化测试,如何使用 @group 选择性地执行测试,并提出了两个新的测试理论:
- 测试覆盖率应该是开发者的辅助工具,而不是管理人员用来考核的指标。
- 自动化测试是为了解放开发者,因此要奔着易学易写的方向去,不要复杂化。
本章代码
https://github.com/heguangyu5/PHPUnit-in-Action-Code/tree/chapter02/hello-world