deminy/counit

使用Swoole加速运行与时间/IO相关的单元测试(例如,睡眠函数调用、数据库查询、API调用等)

0.2.1 2024-01-12 22:17 UTC

This package is auto-updated.

Last update: 2024-08-30 08:57:44 UTC


README

Library Status Latest Stable Version Latest Unstable Version License

此包可以帮助您使用Swoole更快速地运行与时间/IO相关的单元测试(例如,睡眠函数调用、数据库查询、API调用等)。

目录

工作原理

counit 允许使用Swoole在单个PHP进程内并发运行多个与时间/IO相关的测试。 CounitPHPUnit 兼容,这意味着

  1. 测试用例可以像 PHPUnit 那样编写。
  2. 测试用例可以直接在 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 可以使您的测试用例运行得更快。以下是使用 PHPUnitcounit 在真实项目中运行相同测试套件时的比较。在测试套件中,许多测试会调用方法 \Deminy\Counit\Counit::sleep() 以等待某些事情发生(例如,等待数据过期)。

安装

可以使用 Composer 安装此包

composer require deminy/counit --dev

或者在您的 composer.json 文件中,确保包含包 deminy/counit

{
  "require-dev": {
    "deminy/counit": "~0.2.0"
  }
}

在项目中使用 "counit"

  • PHPUnit 那样编写单元测试。但是,为了使这些测试更快,请将那些与时间/IO相关测试按照以下两种方式之一编写(详细信息将在下一节中讨论)
  • 运行单元测试时,请使用二进制可执行文件 ./vendor/bin/counit 而不是 ./vendor/bin/phpunit
  • 已安装Swoole扩展。如果没有安装,counit 将与 PHPUnit(在阻塞模式下)完全一样工作。
  • 可选步骤

示例

文件夹 ./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操作,使测试运行速度更快。对于仅以阻塞模式工作的函数/扩展,此软件包无法使它们的函数调用更快。以下是一些仅以阻塞模式工作的扩展:MongoDBCouchbase和一些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集成。
    • 以全局方式处理@doesNotPerformAssertions注释。
    • 使断言数量与PHPUnit报告的一致。
  • 更好的错误/异常处理。

许可证

MIT许可证。