加速你的 PHPUnit 测试
发布时间 作者 timacdonald
拥有一个快速测试套件和拥有一个快速应用程序一样重要。作为一名开发者,快速获得关于代码状态的反馈可以使开发周期更快。这里我们将介绍一些你可以立即实施的技巧,让你的测试运行得更快。
示例测试套件被故意设计为缓慢的,以模拟更广泛的测试,并强调可能的改进。你实际运行的效果可能会有所不同。
ParaTest
这个包 是一个 PHPUnit 扩展,它运行你的测试套件,但它不是像 PHPUnit 一样串行运行每个测试用例(一个接一个),而是利用你的机器的 CPU 内核来并行运行它们。
要开始使用 ParaTest,你需要通过 composer 安装它作为开发依赖项。
composer require --dev brianium/paratest
现在,我们只需要调用 ParaTest,就像调用 PHPUnit 一样。它会自动根据你机器上可用的核心数量来确定要使用的进程数量。
你可以在上面的控制台输出中看到它已经确定要使用五个并行进程来运行测试套件。相比之下,下面是在串行模式下使用 PHPUnit 运行相同的测试套件。
1.49 秒 vs 6.15 秒!
虽然 ParaTest 会自行确定要启动的进程数量,但你可能需要尝试调整这个数字,以找到适合你机器的最佳设置。要指定进程数量,可以使用 —processes
选项。你应该尝试添加和删除进程,因为更多并不总是意味着更快的测试。
./vendor/bin/paratest --processes 6
注意:在使用 ParaTest 与一个正在访问数据库的测试套件之前,你需要考虑你是如何准备数据库的。如果你正在使用 Laravel 的 RefreshDatabase
特性,你将会遇到问题,因为一个测试可能会在另一个测试试图写入数据库时回滚或迁移数据库。相反,通过使用 DatabaseTransactions
特性来跳过持久化数据,它也不会在测试套件运行期间尝试更改数据库结构。
重新运行失败的测试
PHPUnit 有一个方便的功能,它允许你只重新运行之前运行中失败的测试。如果你正在做红绿 TDD 风格的开发,这将加速你的开发周期。让我们通过一个通过所有现有测试的测试套件来看看这个功能。
接下来,你添加一个新的测试,根据红绿重构模型,它如预期的那样失败了
在对你的代码库进行你认为会使这个新测试通过的更改之后,你想要重新运行套件以验证它是否按预期工作。问题是这个测试套件已经需要 1.3 秒才能运行,所以随着我们不断添加更多测试,花在等待验证代码上的时间会增加。
如果我们能够只运行我们试图解决的失败测试,那不是很好吗?幸运的是,PHPUnit v7.3 添加了这样做的能力.
要使其工作,请在你的 phpunit.xml
配置中添加 cacheResult="true"
。这告诉 PHPUnit 始终记住哪些测试之前失败了。
<?xml version="1.0" encoding="UTF-8"?><phpunit cacheResult="true" backupGlobals="false" ...>
现在,当我们运行测试套件时,PHPUnit 会记住哪些测试失败了,并使用以下选项,我们可以只重新运行那些失败的测试。
./vendor/bin/phpunit --order-by=defects --stop-on-defect
我们不再需要等待整个套件运行才能看到我们试图解决的唯一测试是否通过了。
同样重要的是将缓存文件 .phpunit.result.cache
添加到你的 .gitignore 中,这样它就不会被提交到你的代码库中。
分组慢速测试
PHPUnit 允许你使用 @group
注解 将测试添加到不同的“组”中。如果你有一堆特别慢的测试,可能最好将它们都添加到同一个组中。
class MyTest extends TestCase{ public function test_that_is_fast() { $this->assertTrue(true); } /** * @group slow */ public function test_that_is_slow() { sleep(10); $this->assertTrue(true); } /** * @group slow */ public function test_that_is_slow_2_adrians_revenge() { sleep(10); $this->assertFalse(false); }}
在这个例子中,我们有两个测试需要 10 秒才能运行。我们最不希望的事情是在我们的开发周期中运行这些测试,特别是如果你正在做测试驱动的开发,你需要你的测试套件变得敏捷。
由于这两个较慢的测试都在同一个组中,你现在可以通过使用 PHPUnit 的 --exclude-group
选项将它们从测试运行中排除。
./vendor/bin/phpunit --exclude-group slow
这个命令将运行你所有的测试,除了 slow
组中的那些,这将使你的测试运行得更快。这样分组测试的另一个好处是,你正在记录慢速测试,所以希望你能回来改进它们。
然而,重要的是要有一些检查来确保所有测试,包括慢速测试,在部署到生产环境之前都被运行。一个好的方法是建立一个 CI 管道,它运行你所有的测试。
过滤测试
PHPUnit 有一个 --filter
选项,它接受一个模式来确定运行哪些测试。例如,如果你的所有测试都命名空间化了,你可以通过指定一个命名空间来运行测试的特定子集。以下命令将只运行 Tests\Unit\Models
命名空间中的测试,并排除所有其他测试。
./vendor/bin/phpunit --filter 'Tests\\Unit\\Models'
--filter
选项很灵活,允许通过 methodName
、Class::methodName
甚至通过文件路径过滤,例如 /path/to/my/test.php
。你应该查看 PHPUnit 文档 来了解这个选项,并查看哪些是可能的。
密码哈希轮数
Laravel 默认使用 bcrypt 密码哈希算法,它设计上是缓慢的,并且对系统资源要求很高。如果你的测试验证用户密码,你可以通过设置算法使用的轮数来缩短测试运行时间,因为轮数越多,运行时间越长。
如果你将你的应用程序与 laravel/laravel
项目 中的最新更改保持同步,你会发现哈希轮数可以通过环境变量进行自定义,并且在 phpunit.xml
文件 中已经设置为 4,这是 bcrypt 允许的最小值。
但是,如果你没有跟上最新的更改,你可以在 CreatesApplication
特性中使用 Hash
门面来设置它。
public function createApplication(){ $app = require __DIR__.'/../bootstrap/app.php'; $app->make(Kernel::class)->bootstrap(); // set the bcrypt hashing rounds... Hash::rounds(4); return $app;}
你可以在 Taylor 的这条推文中看到一些关于这种更改的相当不错的结果。
更新你的 CreatesApplication 特性,包括这个 Hash::setRounds 调用,然后比较运行测试的速度差异。 pic.twitter.com/WKGUOgyTCI
— Taylor Otwell (@taylorotwell) 2017年12月19日
内存数据库
使用内存中的 SQLite 数据库是另一种方法,可以提高访问数据库的测试的速度。你可以通过将这两个环境键添加到你的 phpunit.xml
配置中来快速开始使用它。
<php> ... <env name="DB_CONNECTION" value="sqlite"/> <env name="DB_DATABASE" value=":memory:"/></php>
警告:虽然这看起来像是一个简单的胜利,但你应该考虑让数据库与你的生产环境保持一致。如果你在生产环境中使用的是 MySQL,那么你应该意识到使用不同数据库(如 SQLite)进行测试可能会带来潜在的问题。我在 我的特性测试套件设置 中更详细地介绍了这些差异。简而言之,我认为在测试期间与生产环境保持一致比获得微不足道的速度提升更重要。
禁用 Xdebug
如果你没有经常使用 Xdebug,你可能需要考虑在需要它之前禁用它。它会减慢 PHP 执行速度,从而降低你的测试套件速度。如果你每天都使用它进行调试,那么在测试运行时禁用它可能不是一个好选择 - 但在考虑测试套件速度时,这一点需要牢记。
你可以看到,在这个测试套件中,禁用 Xdebug 后,速度有了显著提升。这是启用 Xdebug 运行的套件
以及禁用 Xdebug 的相同测试套件
修复你的缓慢测试
当然,最好的建议是:修复你的缓慢测试!如果你难以查明哪些测试导致你的测试套件运行缓慢,你可能需要查看 PHPUnit Report。它是一个开源工具,允许你通过生成一个云图(如下所示)来可视化你的测试套件性能,其中较大的气泡代表缓慢的测试。这将使你能够找到套件中最慢的测试,并逐步提高它们的性能。