如何在PHPUnit 10+中Mock S3ClientInterface?
PHPUnit 10/11 中Mock AWS S3客户端的最优方案
在PHPUnit 10及以上版本中,addMethods被弃用后,直接Mock S3客户端的魔术方法(比如getObject、doesObjectExistV2)会因为PHPUnit拒绝Mock不存在的方法而报错,只Mock__call又太繁琐。针对这个问题,推荐以下几种靠谱方案:
方案一:使用AWS SDK官方Mock Handler(最推荐)
AWS SDK for PHP本身提供了专门的测试工具MockHandler,可以直接模拟S3客户端的请求和响应,完全不需要处理魔术方法的问题,这也是官方主推的测试方式。
代码示例:
use Aws\S3\S3Client; use Aws\MockHandler; use Aws\Result; // 创建MockHandler实例 $mockHandler = new MockHandler(); // 添加预期响应:模拟doesObjectExistV2返回true $mockHandler->append(new Result(['Exists' => true])); // 添加预期响应:模拟getObject返回指定Body $mockHandler->append(new Result(['Body' => 'foo'])); // 用MockHandler构建S3客户端 $s3 = new S3Client([ 'region' => 'us-east-1', 'version' => 'latest', 'handler' => $mockHandler, ]); // 测试中正常调用方法即可 $this->assertTrue($s3->doesObjectExistV2('MockBucket', 'mockObjects/2d/cf90910456ce55e673dab13a76a3dfd8197aa7')); $response = $s3->getObject([ 'Bucket' => 'MockBucket', 'Key' => 'mockObjects/2d/cf90910456ce55e673dab13a76a3dfd8197aa7', ]); $this->assertEquals('foo', $response['Body']);
如果需要验证请求参数是否正确,可以结合CommandRecorderMiddleware捕获请求:
use Aws\Middleware; use Psr\Http\Message\RequestInterface; $capturedRequests = []; $recorder = Middleware::tap(function (RequestInterface $request) use (&$capturedRequests) { $capturedRequests[] = $request; }); // 把记录器中间件加到MockHandler $handler = $mockHandler->append($recorder); // 调用方法后检查捕获的请求 $s3->doesObjectExistV2('MockBucket', 'test-key'); $request = $capturedRequests[0]; $this->assertStringContainsString('/MockBucket/test-key', (string)$request->getUri());
方案二:自定义接口+Mock(适合必须用PHPUnit Mock的场景)
如果你的代码依赖S3ClientInterface,可以自己定义一个包含所需方法的接口——因为S3客户端的魔术方法特性,PHP会自动适配这个接口的调用,之后直接Mock这个自定义接口即可。
代码示例:
// 自定义包含所需方法的接口 interface MyS3ClientInterface { public function doesObjectExistV2(string $bucket, string $key): bool; public function getObject(array $params): array; } // 测试中Mock这个自定义接口 $s3 = $this->createMock(MyS3ClientInterface::class); $s3->expects($this->once()) ->method('doesObjectExistV2') ->with('MockBucket', 'mockObjects/2d/cf90910456ce55e673dab13a76a3dfd8197aa7') ->willReturn(true); $s3->expects($this->once()) ->method('getObject') ->with(['Bucket' => 'MockBucket', 'Key' => 'mockObjects/2d/cf90910456ce55e673dab13a76a3dfd8197aa7']) ->willReturn(['Body' => 'foo']);
注意:要确保自定义接口的方法签名和S3客户端的实际调用逻辑一致,避免出现调用错误。
方案三:优化Mock __call方法的写法
如果以上两种方案都不适用,只能Mock__call的话,可以通过回调函数区分不同方法调用,减少重复代码:
$s3 = $this->createMock(S3ClientInterface::class); $s3->expects($this->exactly(2)) ->method('__call') ->willReturnCallback(function (string $method, array $arguments) { return match ($method) { 'doesObjectExistV2' => $arguments[0] === 'MockBucket' && $arguments[1] === 'mockObjects/2d/cf90910456ce55e673dab13a76a3dfd8197aa7', 'getObject' => $arguments[0] === ['Bucket' => 'MockBucket', 'Key' => 'mockObjects/2d/cf90910456ce55e673dab13a76a3dfd8197aa7'] ? ['Body' => 'foo'] : null, default => null, }; });
这种方式比单独处理每个调用简洁,但还是不如官方Mock Handler直观,仅作为备选方案。
内容的提问来源于stack exchange,提问作者donatJ
相关产品推荐
相关产品推荐

