本文目录导读:

在 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);
}
}
注意事项
- 超时处理:始终为异步测试设置超时
- 隔离测试:每个异步测试应该独立运行
- 错误处理:确保捕获所有异步异常
- 资源清理:测试后关闭连接和资源
记住选择测试方法时要基于你实际使用的异步框架,对于小项目,传统的 PHPUnit + 回调/Promise 即可;对于复杂应用,建议使用专门的异步测试工具。