如何使用PHPUnit对并发读/写进行单元测试?

我最近在实时应用程序上遇到了一个问题。我意识到我有越来越多的并发异常和数据库锁定。

基本上,我启动了一个事务,该事务需要在同一个表上提交 a 和 an。SELECTINSERT

但是由于负载真的很重,每个事务都会锁定表,在大多数情况下,它非常快,不会引起任何问题,但有一个点,锁开始等待越来越多。

我能够通过调整查询来解决这个问题。

不过,现在,我想用PHPUnit编写一些测试来验证我的修复并避免任何回归。

我无法找到任何关于如何做到这一点的材料。

由于PHP不是多线程的,我不知道如何在单个测试中运行并发查询以进行验证。

基本上,我希望能够在单个测试中运行多个调用,以确保一切正常。

我知道我可以通过直接查询http服务器并加载整个应用程序来尝试进行一些高级测试,但是由于我的问题来自独立库,因此我宁愿针对自身进行测试。

有什么想法吗?


答案 1

简短的回答是,没有好的方法可以使用PHPUnit在实际数据库上测试并发读/写。它根本不是这项工作的正确工具。

但是,测试此内容的良好解决方案有几个方面。首先,可以(并且应该)编写代码来处理每个可能的问题。像PostgreSQL这样的数据库系统会在锁定和事务问题时立即失败。为了优雅地处理它,我使用看起来像这样的代码(伪代码,也用于回答另一个问题):

begin transaction
while not successful and count < 5
    try 
        execute sql
        commit
    except
        if error code is '40P01' or '55P03'
            # Deadlock or lock not available
            sleep a random time (200 ms to 1 sec) * number of retries
        else if error code is '40001' or '25P02'
            # "In failed sql transaction" or serialized transaction failure
            rollback
            sleep a random time (200 ms to 1 sec) * number of retries
            begin transaction
        else if error message is 'There is no active transaction'
            sleep a random time (200 ms to 1 sec) * number of retries
            begin transaction
    increment count

然后创建两组测试:一组应确认代码是否正确处理情况(即单元测试)。另一组测试是针对环境的(即集成/功能测试)。

单元测试

我发现在连接到数据库的PHPUnit测试中很难重现这种情况,并且使用真实数据库不适合真正的单元测试。相反,请创建引发各种数据库异常的 PDO 存根和单元测试。这确认代码按预期工作,但不测试任何真实数据库上的并发性。请记住,单元测试仅用于确认代码编写正确,而不是用于测试第三方软件。

$iterationCount = 0;
$db->runInTransaction(function() use (&$iterationCount) {
    $iterationCount++;
    if ($iterationCount === 1) {
        $exception = new PDOExceptionStub('Deadlock');
        $exception->setCode('40P01');
        throw $exception;
    }
});

// First time fails, second time succeeds
$this->assertEquals(2, $iterationCount, 'Expected 2 iterations of runInTransaction');

编写一套完整的测试,这些测试不连接到数据库,但确认逻辑。

集成测试

正如你所发现的,PHPUnit 根本不是执行负载测试的正确工具。它不适合比顺序单元和集成测试更复杂的内容。您可以同时运行 PHPUnit 的多个实例,以增加数据库的负载。但是,我发现这超出了它的本意,而且它不能帮助您监视数据库是否存在问题。因此,我认为没有办法绕过您希望避免的更高级别的测试。

但是,可以在不运行完整应用程序的情况下测试您的库。我将创建最简单的应用程序,仅用于测试它。它可以有一个或多个连接到数据库的 CLI 脚本。可以多次生成这些脚本以将负载放在数据库上。或者使用该库创建一个简单的网页,并使用许多负载测试应用程序中的任何一个来测试它。


答案 2

推荐