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 注释, 给 DataProviderTestsayHelloWithDataProviderTest 加上 @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

再来回顾下我们写的测试用例,会发现:

  1. 非法字符我们仅测试了一个“!”号
  2. 有效字符空格我们没有测试

可是我们的测试覆盖率已经 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 选择性地执行测试,并提出了两个新的测试理论:

  1. 测试覆盖率应该是开发者的辅助工具,而不是管理人员用来考核的指标。
  2. 自动化测试是为了解放开发者,因此要奔着易学易写的方向去,不要复杂化。

本章代码

https://github.com/heguangyu5/PHPUnit-in-Action-Code/tree/chapter02/hello-world