单元测试非常之重要,好的项目,测试代码比项目代码还多,即便在开发阶段不写单元测试,在项目上线后,功能迭代的时候如果项目很大,小的单元测试可以节省繁琐的深度测试的时间。
这篇文章旨在入门 phpunit,虽然大部分框架都已经集成了 phpunit 来作为单元测试,但是你真的知道这里面的过程和细节吗?
安装PHPUnit
官网:http://www.phpunit.cn/
composer global require phpunit/phpunit
可以看到 composer 会提示将 phpunit 安装在哪个目录了。
将 D:\composer\vendor\bin
添加到环境变量中,因为 phpunit 是一个命令。
查看是否安装成功:phpunit --version
PHP version 7.2.1
phpunit version 8.5.14
示例
项目结构,使用的是官网的例子。
├── phpunit.xml
├── src
│ └── Email.php
└── tests
└── EmailTest.php
Email.php
<?php
class Email
{
private $email;
private function __construct(string $email)
{
$this->ensureIsValidEmail($email);
$this->email = $email;
}
public static function fromString(string $email): self
{
return new self($email);
}
public function __toString(): string
{
return $this->email;
}
private function ensureIsValidEmail(string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException(
sprintf(
'"%s" is not a valid email address',
$email
)
);
}
}
}
EmailTest.php
<?php
use PHPUnit\Framework\TestCase;
include_once('D:/dev/php/magook/trunk/server/phpunit-test/src/Email.php');
class EmailTest extends TestCase
{
public function testCanBeCreatedFromValidEmailAddress(): void
{
$this->assertInstanceOf(
Email::class,
Email::fromString('user@example.com')
);
}
public function testCannotBeCreatedFromInvalidEmailAddress(): void
{
$this->expectException(InvalidArgumentException::class);
Email::fromString('invalid');
}
public function testCanBeUsedAsString(): void
{
$this->assertEquals(
'user@example.com',
Email::fromString('user@example.com')
);
}
}
进入项目根目录
> phpunit tests/EmailTest
PHPUnit 8.5.14 by Sebastian Bergmann and contributors.
Warning: Invocation with class name is deprecated
... 3 / 3 (100%)
Time: 1.32 seconds, Memory: 4.00 MB
OK (3 tests, 3 assertions)
说明
phpunit
命令后面跟的是测试文件的名字。phpunit
是一个测试框架,它的原理是将要测试的文件加载到 phpunit 框架中并运行,所以EmailTest.php
文件中加载的类PHPUnit\Framework\TestCase
不是在本项目中,而是在 phpunit 框架中的。为了编码方便,很多框架都会将 phpunit 加载到自己的vendor
中。- 至于
Warning: Invocation with class name is deprecated
,暂时可以忽略。
使用 composer 实现 autoload 自动加载
在根目录下新建 composer.json
{
"autoload": {
"classmap": [
"src/"
]
},
"require-dev": {
"phpunit/phpunit": "^8"
}
}
然后运行 composer install
然后修改代码
Email.php
<?php
namespace src;
use InvalidArgumentException;
......
EmailTest.php
<?php
use PHPUnit\Framework\TestCase;
use src\Email;
// include_once('D:/dev/php/magook/trunk/server/phpunit-test/src/Email.php');
......
好了,我们来执行测试:phpunit tests/EmailTest
报错:Error: Class 'src\Email' not found
,这是为什么呢?还是因为 phpunit 的运行原理,我们需要告诉它此项目的autoload方法。例如 Laravel, TP 这样的集成框架,不仅需要自动加载,还需要一些初始化操作,这些都需要告诉 phpunit。
修改命令
>phpunit --bootstrap vendor/autoload.php tests/EmailTest
PHPUnit 8.5.14 by Sebastian Bergmann and contributors.
Warning: Invocation with class name is deprecated
... 3 / 3 (100%)
Time: 309 ms, Memory: 4.00 MB
OK (3 tests, 3 assertions)
为了简化命令,phpunit 允许你在根目录下的 phpunit.xml
文件里来配置你的启动设置。
phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="Application Test Suite">
<directory suffix="Test.php">tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
</phpunit>
执行测试命令
正常运行,并且有彩色输出了。
为了加深印象,我们来对比一下 Laravel 框架的 phpunit.xml
文件
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="bootstrap/app.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Application Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
</php>
</phpunit>
在其 bootstrap/app.php
中指定了autoload和一些初始化操作。
命令行参数
绝大部分时候我们可能常用来测试某个方法,因此只需要执行测试文件中的某个方法即可,而不是整个文件。这就需要了解 phpunit 的命令行参数了。文档:https://phpunit.readthedocs.io/en/8.5/textui.html
phpunit --help
...
Test Selection Options:
--filter <pattern> Filter which tests to run
--testsuite <name> Filter which testsuite to run
--group <name> Only runs tests from the specified group(s)
--exclude-group <name> Exclude tests from the specified group(s)
--list-groups List available test groups
--list-suites List available test suites
--list-tests List available tests
--list-tests-xml <file> List available tests in XML format
--test-suffix <suffixes> Only search for test in files with specified suffix(es). Default: Test.php,.phpt
...
Test Selection Options
模块参数就是选择执行哪些测试方法。
最简单的方式就是 --filter
直接填上要执行的方法名,当然它毕竟是一个正则匹配,这取决于你的方法命名是否唯一。
phpunit --filter "testCanBeUsedAsString" tests/EmailTest
总结
- 针对类
Class
的测试写在类ClassTest
中。 ClassTest
继承自PHPUnit\Framework\TestCase
。- 测试都是命名为
test*
的公用方法。也可以在方法的文档注释块中使用@test
标注将其标记为测试方法。
注意
在使用 phpunit 的过程中你大概率会遇到版本的问题,正如我上面所说,大部分框架都会将 phpunit 集成过来,而它集成的 phpunit 是哪个版本呢,和你本地全局安装的 phpunit 是否能兼容呢?
查看全局的 phpunit 版本:phpunit --version
查看项目中的 phpunit 版本:进入到项目 vendor/bin/phpunit --version
比如我全局的是 8.5.14
,而项目的是 7.5.20
,当我使用全局命令执行测试会报错:
> phpunit tests/DzRead3Test
In TestRunner.php line 155:
Argument 3 passed to PHPUnit\TextUI\TestRunner::doRun() must be of the type boolean, array given, called in D:\composer\vendor\phpunit\phpunit\src\TextUI\Command.php on line 23
而实际上 phpunit 经常会出现版本不兼容的情况。于是你应该使用项目中的 phpunit 命令。
> vendor/bin/phpunit tests/DzRead3Test
PHPUnit 7.5.20 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 785 ms, Memory: 10.00 MB
OK (1 test, 1 assertion)
或者将全局 phpunit 降级
> composer global require phpunit/phpunit 7.5.20 --with-all-dependencies
> phpunit --version
PHPUnit 7.5.20 by Sebastian Bergmann and contributors.
> phpunit tests/DzRead3Test
PHPUnit 7.5.20 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 1.83 seconds, Memory: 10.00 MB
OK (1 test, 1 assertion)