deminy / counit
使用Swoole加速运行与时间/IO相关的单元测试(例如,睡眠函数调用、数据库查询、API调用等)
Requires
- php: >=7.2
- phpunit/phpunit: ~8.0 || ~9.0
Requires (Dev)
- swoole/ide-helper: >=4.6
Suggests
- ext-swoole: To run time/IO related unit tests (e.g., sleep function calls, database queries, API calls, etc) faster.
README
此包可以帮助您使用Swoole更快速地运行与时间/IO相关的单元测试(例如,睡眠函数调用、数据库查询、API调用等)。
目录
工作原理
包 counit 允许使用Swoole在单个PHP进程内并发运行多个与时间/IO相关的测试。 Counit 与 PHPUnit 兼容,这意味着
- 测试用例可以像 PHPUnit 那样编写。
- 测试用例可以直接在 PHPUnit 下运行。
counit 的典型测试用例如下
use Deminy\Counit\TestCase; // Here is the only change made for counit, comparing to test cases for PHPUnit. class SleepTest extends TestCase { public function testSleep(): void { $startTime = time(); sleep(3); $endTime = time(); self::assertEqualsWithDelta(3, ($endTime - $startTime), 1, 'The sleep() function call takes about 3 seconds to finish.'); } }
与 PHPUnit 相比,counit 可以使您的测试用例运行得更快。以下是使用 PHPUnit 和 counit 在真实项目中运行相同测试套件时的比较。在测试套件中,许多测试会调用方法 \Deminy\Counit\Counit::sleep() 以等待某些事情发生(例如,等待数据过期)。
安装
可以使用 Composer 安装此包
composer require deminy/counit --dev
或者在您的 composer.json 文件中,确保包含包 deminy/counit
{ "require-dev": { "deminy/counit": "~0.2.0" } }
在项目中使用 "counit"
- 像 PHPUnit 那样编写单元测试。但是,为了使这些测试更快,请将那些与时间/IO相关测试按照以下两种方式之一编写(详细信息将在下一节中讨论)
- 全局样式(推荐):使用类 Deminy\Counit\TestCase 而不是 PHPUnit\Framework\TestCase 作为基类。
- 逐个样式:将每个测试用例包裹在方法 Deminy\Counit\Counit::create() 的回调函数中,并使用方法 Deminy\Counit\Counit::sleep() 而不是PHP函数 sleep()。
- 运行单元测试时,请使用二进制可执行文件 ./vendor/bin/counit 而不是 ./vendor/bin/phpunit。
- 已安装Swoole扩展。如果没有安装,counit 将与 PHPUnit(在阻塞模式下)完全一样工作。
- 可选步骤
- 如文件 phpunit.xml.dist 中所示,使用PHPUnit扩展 Deminy\Counit\CounitExtension。这是为了在最后打印总结信息之前等待整个测试套件完成。
示例
文件夹 ./tests/unit/global 和 ./tests/unit/case-by-case 包含一些示例测试,其中我们包含了以下与时间相关的测试
- 测试慢速HTTP请求。
- 测试长时间运行的MySQL查询。
- 测试Redis中的数据过期。
- 测试PHP中的sleep()函数调用。
设置测试环境
要运行示例测试,请首先启动Docker容器并安装Composer包
docker-compose up -d
docker compose exec -ti swoole composer install -n
已启动五个容器:一个PHP容器、一个Swoole容器、一个Redis容器、一个MySQL容器和一个Web服务器。PHP容器未安装Swoole扩展,而Swoole容器已安装并启用了该扩展。
如前所述,测试用例可以像PHPUnit的测试用例一样编写。然而,为了使用counit更快地运行与时间/IO相关的测试,我们需要在编写这些测试用例时进行一些调整;这些调整可以以两种不同的风格进行。
“全局”风格(推荐)
在这种风格中,每个测试用例会自动在单独的协程中运行。
对于这种风格编写的测试用例,您需要做的唯一更改是在现有测试用例中,将基类从PHPUnit\Framework\TestCase更改为Deminy\Counit\TestCase。
全局风格的典型测试用例如下所示
use Deminy\Counit\TestCase; // Here is the only change made for counit, comparing to test cases for PHPUnit. class SleepTest extends TestCase { public function testSleep(): void { $startTime = time(); sleep(3); $endTime = time(); self::assertEqualsWithDelta(3, ($endTime - $startTime), 1, 'The sleep() function call takes about 3 seconds to finish.'); } }
当在测试用例中定义自定义方法setUpBeforeClass()和tearDownAfterClass()时,请确保在这些自定义方法中相应地调用其父方法。
这种风格假设测试用例中没有立即的断言,也没有在sleep()函数调用或协程友好的IO操作之前的断言。以下测试用例仍然有效,但在测试时会触发一些警告消息
class GlobalTest extends Deminy\Counit\TestCase { public function testAssertionSuppression(): void { self::assertTrue(true, 'Trigger an immediate assertion.'); // ...... } }
我们可以使用“逐个案例”风格(下文将讨论)重写此测试类,以消除警告消息。
要找到更多这种风格的测试用例,请检查./tests/unit/global(测试套件“global”)文件夹下的测试。
"逐个"样式
在这种风格中,您将直接对测试用例进行更改以使其异步运行。
对于这种风格的测试用例,在需要等待PHP执行或执行IO操作的地方,我们需要在测试用例中使用类Deminy\Counit\Counit。通常,以下方法调用将被使用
- 使用方法Deminy\Counit\Counit::create()包装测试用例。
- 使用方法Deminy\Counit\Counit::sleep()代替PHP函数sleep()等待PHP执行。如果您想使其他与IO相关的测试异步运行,您需要了解一些Swoole知识。
逐个案例风格的典型测试用例如下所示
use Deminy\Counit\Counit; use PHPUnit\Framework\TestCase; class SleepTest extends TestCase { public function testSleep(): void { Counit::create(function () { // To create a new coroutine manually to run the test case. $startTime = time(); Counit::sleep(3); // Call this method instead of PHP function sleep(). $endTime = time(); self::assertEqualsWithDelta(3, ($endTime - $startTime), 1, 'The sleep() function call takes about 3 seconds to finish.'); }); } }
如果您需要抑制警告消息“此测试未执行任何断言”或使断言数量匹配,可以在创建新协程时包含第二个参数
use Deminy\Counit\Counit; use PHPUnit\Framework\TestCase; class SleepTest extends TestCase { public function testSleep(): void { Counit::create( // To create a new coroutine manually to run the test case. function () { $startTime = time(); Counit::sleep(3); // Call this method instead of PHP function sleep(). $endTime = time(); self::assertEqualsWithDelta(3, ($endTime - $startTime), 1, 'The sleep() function call takes about 3 seconds to finish.'); }, 1 // Optional. To suppress warning message "This test did not perform any assertions", and to make the counters match. ); } }
要找到更多这种风格的测试用例,请检查./tests/unit/case-by-case(测试套件“case-by-case”)文件夹下的测试。
比较
我们将在此处以不同的环境运行测试,有或没有Swoole。
#1
使用PHPUnit运行测试套件
# To run test suite "global": docker compose exec -ti php ./vendor/bin/phpunit --testsuite global # or, docker compose exec -ti swoole ./vendor/bin/phpunit --testsuite global # To run test suite "case-by-case": docker compose exec -ti php ./vendor/bin/phpunit --testsuite case-by-case # or, docker compose exec -ti swoole ./vendor/bin/phpunit --testsuite case-by-case
#2
使用counit运行测试套件(无Swoole)
# To run test suite "global": docker compose exec -ti php ./counit --testsuite global # To run test suite "case-by-case": docker compose exec -ti php ./counit --testsuite case-by-case
#3
使用counit运行测试套件(启用Swoole扩展)
# To run test suite "global": docker compose exec -ti swoole ./counit --testsuite global # To run test suite "case-by-case": docker compose exec -ti swoole ./counit --testsuite case-by-case
前两组命令的完成时间大约相同。最后一组命令使用counit并在Swoole容器(其中启用了Swoole扩展)中运行;因此,它比其他方法更快。
其他注意事项
由于此包允许同时运行多个测试,因此我们不应在不同的测试中使用相同的资源;否则,可能会发生竞态条件。例如,如果多个测试使用相同的Redis键,其中一些测试可能会偶尔失败。在这种情况下,我们应在不同的测试用例中使用不同的Redis键。可以使用方法\Deminy\Counit\Helper::getNewKey()和\Deminy\Counit\Helper::getNewKeys()生成随机且唯一的测试键。
该包最适合具有函数调用sleep()的测试;它还可以帮助更快地运行一些与IO相关的测试,但有一些限制。以下是此包的限制列表
- 该软件包通过同时执行时间/IO操作,使测试运行速度更快。对于仅以阻塞模式工作的函数/扩展,此软件包无法使它们的函数调用更快。以下是一些仅以阻塞模式工作的扩展:MongoDB、Couchbase和一些ODBC驱动程序。
- 该软件包在运行于PHPUnit时的工作方式并不完全相同。
- 即使标记为已完成(由PHPUnit标记),测试可能仍未完成。因此,标记为“通过”(由PHPUnit标记)的测试在counit下可能仍会在以后失败。因此,检查所有测试用例是否都通过的最可靠方法是检查counit的退出代码。
- 报告的断言数量可能与PHPUnit不同。
- 某些异常/错误处理/报告方式不同。
本地开发
存在用于运行示例测试的预构建镜像deminy/counit。以下为构建镜像的命令
docker build -t deminy/counit:php-only -f ./dockerfiles/php/Dockerfile . docker build -t deminy/counit:swoole-enabled -f ./dockerfiles/swoole/Dockerfile .
替代方案
此软件包允许使用Swoole在单个PHP进程中运行多个与时间/IO相关的测试,这意味着所有测试都可以在一个PHP进程中运行。要了解其具体工作方式,我建议查看这个免费的在线演讲:CSP Programming in PHP(以及幻灯片)。
在PHP生态系统中,还有其他选项可以并行运行单元测试,大多数最终都使用多进程。
- PHPUnit中的进程隔离。这允许在单独的PHP进程中运行测试。
- brianium/paratest软件包
- pestphp/pest软件包
待办事项
- 更好地与PHPUnit集成。
- 以全局方式处理@doesNotPerformAssertions注释。
- 使断言数量与PHPUnit报告的一致。
- 更好的错误/异常处理。
许可证
MIT许可证。