PHP 怎么测试异步代码

wen PHP项目 2

本文目录导读:

PHP 怎么测试异步代码

  1. 使用 PHPUnit 测试协程
  2. 使用专门的异步测试库
  3. 使用回调驱动的测试
  4. 使用 Swoole 的测试辅助
  5. 使用异步测试框架
  6. 最佳实践
  7. 注意事项

在 PHP 中测试异步代码是个挑战,因为 PHP 本身是同步语言,根据你使用的异步方案(如 Swoole、ReactPHP、Amp 或协程),有几种不同的测试方法。

使用 PHPUnit 测试协程

Swoole 协程测试

<?php
use PHPUnit\Framework\TestCase;
use Swoole\Coroutine;
class AsyncTest extends TestCase
{
    public function testCoroutineAsyncOperation()
    {
        $result = Coroutine\run(function () {
            // 模拟异步操作
            $value = co::exec('sleep 1 && echo "done"');
            return $value['output'];
        });
        $this->assertEquals('done', $result);
    }
}

ReactPHP 测试

<?php
use PHPUnit\Framework\TestCase;
use React\EventLoop\Factory;
use React\Promise\Promise;
class ReactAsyncTest extends TestCase
{
    private $loop;
    protected function setUp(): void
    {
        $this->loop = Factory::create();
    }
    public function testPromiseResolve()
    {
        $promise = new Promise(function ($resolve) {
            $this->loop->futureTick(function () use ($resolve) {
                $resolve('async-result');
            });
        });
        $result = null;
        $promise->then(function ($value) use (&$result) {
            $result = $value;
        });
        // 运行事件循环直到 Promise 完成
        $this->loop->run();
        $this->assertEquals('async-result', $result);
    }
}

使用专门的异步测试库

amphp/phpunit-util

<?php
use PHPUnit\Framework\TestCase;
use Amp\Loop;
use Amp\PHPUnit\AsyncTest;
class AsyncAmpTest extends TestCase
{
    use AsyncTest; // 使用 Amp 的异步测试特质
    public function testAsyncOperation()
    {
        return \Amp\call(function () {
            // 等待异步操作
            $result = yield \Amp\delay(100, 'async-data');
            $this->assertEquals('async-data', $result);
        });
    }
}

使用回调驱动的测试

<?php
use PHPUnit\Framework\TestCase;
use React\Http\Browser;
class CallbackAsyncTest extends TestCase
{
    public function testHttpRequest()
    {
        $loop = React\EventLoop\Factory::create();
        $client = new Browser($loop);
        $promise = $client->get('https://api.example.com/data');
        // 使用 Promise 测试辅助方法
        $promise->then(
            function ($response) {
                $this->assertEquals(200, $response->getStatusCode());
                $this->assertNotEmpty($response->getBody());
            },
            function ($exception) {
                $this->fail('Request failed: ' . $exception->getMessage());
            }
        );
        $loop->run(); // 执行事件循环
    }
}

使用 Swoole 的测试辅助

<?php
use PHPUnit\Framework\TestCase;
use Swoole\Coroutine;
use Swoole\Coroutine\Channel;
class SwooleChannelTest extends TestCase
{
    public function testChannelCommunication()
    {
        $channel = new Channel(1);
        go(function () use ($channel) {
            // 模拟异步任务
            $channel->push('task-result');
        });
        // 在协程上下文中使用
        Coroutine\run(function () use ($channel) {
            $result = $channel->pop(); // 会等待数据
            $this->assertEquals('task-result', $result);
        });
    }
}

使用异步测试框架

Pest 的异步支持

<?php
use function Pest\Swoole\test;
test('async swoole coroutine', function () {
    $swoole = swoole_async_dns_lookup("www.test.com", function ($host, $ip) {
        test('returns valid IP', function () use ($ip) {
            expect($ip)->not->toBeEmpty();
        });
    });
});

最佳实践

创建测试辅助方法

<?php
trait AsyncTestHelper
{
    private function assertAsyncResult(callable $executor, $expected, $timeout = 5)
    {
        $loop = Factory::create();
        $timer = null;
        $completed = false;
        $promise = call_user_func($executor);
        $promise->then(
            function ($result) use (&$completed, $expected) {
                $this->assertEquals($expected, $result);
                $completed = true;
            },
            function ($error) use (&$completed) {
                $this->fail($error->getMessage());
                $completed = true;
            }
        );
        // 添加超时保护
        $loop->addTimer($timeout, function () use (&$completed) {
            if (!$completed) {
                throw new \RuntimeException('Async operation timeout');
            }
            $completed = true;
        });
        $loop->run();
    }
    public function testWithHelper()
    {
        $this->assertAsyncResult(
            function () {
                // 返回 Promise
                return $this->someAsyncMethod();
            },
            'expected-result'
        );
    }
}

使用 Swoole 测试工具

<?php
class SwooleTest extends TestCase
{
    public static function setUpBeforeClass(): void
    {
        // 启动 Swoole HTTP 服务
        $server = new Swoole\Http\Server('127.0.0.1', 9501);
        $server->on('request', function ($request, $response) {
            $response->end('hello world');
        });
        $server->start();
    }
    public function testSwooleServer()
    {
        $response = file_get_contents('http://127.0.0.1:9501');
        $this->assertEquals('hello world', $response);
    }
}

注意事项

  1. 超时处理:始终为异步测试设置超时
  2. 隔离测试:每个异步测试应该独立运行
  3. 错误处理:确保捕获所有异步异常
  4. 资源清理:测试后关闭连接和资源

记住选择测试方法时要基于你实际使用的异步框架,对于小项目,传统的 PHPUnit + 回调/Promise 即可;对于复杂应用,建议使用专门的异步测试工具。

抱歉,评论功能暂时关闭!