PHP中的VCR模式测试:录制与回放外部API请求以实现离线集成测试
大家好,今天我们来聊聊如何在PHP中使用VCR模式进行集成测试,特别是那些依赖外部API的场景。 在真实的开发环境中,我们的代码经常需要与各种外部服务进行交互,例如支付网关、邮件服务、第三方数据平台等等。 在这种情况下,进行集成测试往往会面临一些挑战:
- 依赖性高: 测试环境需要配置完整的外部依赖,这可能很复杂且耗时。
- 不稳定: 外部服务可能不稳定,导致测试结果不可靠。
- 速度慢: 每次测试都需要真实地调用外部API,速度较慢。
- 成本高: 部分外部API可能按调用次数收费,频繁测试会增加成本。
- 难以控制: 无法模拟外部API的各种异常情况。
VCR模式就是为了解决这些问题而生的。 它的核心思想是:先将与外部API的交互录制下来,然后在测试时回放这些录制的内容,从而避免真实地调用外部API。 这样我们就可以在离线环境下进行集成测试,提高测试速度和可靠性,降低成本,并且可以模拟各种异常情况。
VCR模式的核心概念
VCR模式主要涉及以下几个核心概念:
- Cassette(磁带): 用于存储录制下来的HTTP请求和响应。 通常以YAML或JSON格式存储。
- Request(请求): 记录了发往外部API的HTTP请求的所有信息,包括URL、Method、Headers、Body等。
- Response(响应): 记录了外部API返回的HTTP响应的所有信息,包括Status Code、Headers、Body等。
- VCR(录像机): 负责拦截HTTP请求,并将请求和响应记录到Cassette中,或者从Cassette中读取响应并返回。
如何在PHP中使用VCR
在PHP中,我们可以使用php-vcr/php-vcr这个库来实现VCR模式。
1. 安装 php-vcr/php-vcr
可以使用Composer进行安装:
composer require php-vcr/php-vcr
2. 配置 VCR
在使用VCR之前,需要进行一些基本的配置。 例如,指定Cassette的存储目录:
<?php
use VCRVCR;
VCR::configure()
->setCassettePath('tests/cassettes') // 指定Cassette存储目录
->enableLibraryHooks(['curl', 'stream_wrapper']) // 启用curl和stream_wrapper的钩子
->setMode('once'); // 设置录制模式为once
3. 录制和回放
使用VCR::insertCassette()方法开始录制,使用VCR::eject()方法结束录制。 在录制期间,所有发往外部API的请求都会被记录到指定的Cassette中。
<?php
use VCRVCR;
use GuzzleHttpClient;
// 启动VCR,指定Cassette名称
VCR::insertCassette('get_user_profile');
// 创建Guzzle HTTP Client
$client = new Client();
// 发送请求
$response = $client->get('https://api.example.com/user/123');
// 断言响应状态码
assertEquals(200, $response->getStatusCode());
// 断言响应内容
$body = json_decode($response->getBody(), true);
assertEquals('John Doe', $body['name']);
// 停止VCR
VCR::eject();
第一次运行这段代码时,VCR会拦截https://api.example.com/user/123的请求,并将其和响应保存到tests/cassettes/get_user_profile.yml文件中。
以后再次运行这段代码时,VCR会直接从get_user_profile.yml文件中读取响应,而不会真实地调用https://api.example.com/user/123。
4. Cassette 文件内容示例
tests/cassettes/get_user_profile.yml 文件的内容可能如下所示:
interactions:
- request:
method: GET
uri: 'https://api.example.com/user/123'
body: ''
headers:
Host:
- 'api.example.com'
User-Agent:
- 'GuzzleHttp/7'
response:
status:
code: 200
message: OK
headers:
Content-Type:
- 'application/json'
body: '{"id": 123, "name": "John Doe", "email": "[email protected]"}'
http_version: '1.1'
version: 1
VCR的配置选项
VCR::configure()方法提供了很多配置选项,可以根据实际需求进行调整:
| 配置项 | 描述 |
|---|---|
| cassette_path | 指定Cassette的存储目录。 |
| mode | 指定VCR的运行模式。 可以是once(只录制一次)、rewrite(每次都重新录制)、playback(只回放,不录制)。 |
| library_hooks | 指定需要拦截的HTTP客户端库。 默认情况下,VCR会拦截curl和stream_wrapper。 |
| whitelist | 指定允许真实调用的URL白名单。 如果请求的URL不在白名单中,VCR会尝试从Cassette中读取响应。 |
| blacklist | 指定禁止真实调用的URL黑名单。 如果请求的URL在黑名单中,VCR会抛出异常。 |
| filter_request_callback | 指定一个回调函数,用于在录制请求之前修改请求。 例如,可以用于去除敏感信息。 |
| filter_response_callback | 指定一个回调函数,用于在录制响应之前修改响应。 例如,可以用于去除敏感信息。 |
录制模式 (Mode)
VCR 提供了几种录制模式,可以通过 VCR::configure()->setMode() 来设置:
once(默认): 如果 Cassette 文件不存在,则录制请求和响应并保存到文件。如果 Cassette 文件已经存在,则回放文件中的响应,不会发起真实的网络请求。rewrite: 每次都重新录制请求和响应,覆盖已有的 Cassette 文件。playback: 只回放 Cassette 文件中的响应,如果 Cassette 文件不存在或者请求在 Cassette 中找不到对应的响应,则会抛出异常。 此模式适用于确保测试不会意外发起网络请求。passthrough: 禁用 VCR 的录制和回放功能,所有请求都会真实地发送到外部 API。 适用于临时禁用 VCR 进行调试或测试某些特定场景。
真实案例:测试支付功能
假设我们需要测试一个支付功能,该功能需要调用第三方支付网关的API。
1. 创建测试用例
<?php
use PHPUnitFrameworkTestCase;
use VCRVCR;
use GuzzleHttpClient;
class PaymentTest extends TestCase
{
private $client;
protected function setUp(): void
{
$this->client = new Client([
'base_uri' => 'https://api.example-payment-gateway.com',
]);
}
public function testSuccessfulPayment()
{
VCR::insertCassette('successful_payment');
$response = $this->client->post('/payments', [
'json' => [
'amount' => 100,
'currency' => 'USD',
'card_number' => '4111111111111111',
'expiry_date' => '12/24',
'cvv' => '123',
],
]);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody(), true);
$this->assertEquals('success', $data['status']);
VCR::eject();
}
public function testFailedPaymentDueToInvalidCard()
{
VCR::insertCassette('failed_payment_invalid_card');
$response = $this->client->post('/payments', [
'json' => [
'amount' => 100,
'currency' => 'USD',
'card_number' => '4000000000000000', // Invalid card number
'expiry_date' => '12/24',
'cvv' => '123',
],
'http_errors' => false, // 允许Guzzle处理非200状态码
]);
$this->assertEquals(400, $response->getStatusCode());
$data = json_decode($response->getBody(), true);
$this->assertEquals('invalid_card', $data['error_code']);
VCR::eject();
}
}
2. 运行测试用例
第一次运行测试用例时,VCR会拦截对https://api.example-payment-gateway.com/payments的请求,并将其和响应保存到tests/cassettes/successful_payment.yml和tests/cassettes/failed_payment_invalid_card.yml文件中。
以后再次运行测试用例时,VCR会直接从这些文件中读取响应,而不会真实地调用支付网关的API。
3. 模拟异常情况
通过修改Cassette文件,我们可以模拟各种异常情况,例如:
- 支付网关服务不可用: 可以将Cassette文件中的响应状态码改为500。
- 支付金额不足: 可以修改请求中的金额,并修改Cassette文件中的响应内容。
- 信用卡已过期: 可以修改请求中的信用卡有效期,并修改Cassette文件中的响应内容。
过滤敏感信息
在录制API请求时,我们可能需要过滤掉一些敏感信息,例如API密钥、密码、信用卡号等。 可以使用filter_request_callback和filter_response_callback配置项来实现:
<?php
use VCRVCR;
VCR::configure()
->setCassettePath('tests/cassettes')
->enableLibraryHooks(['curl', 'stream_wrapper'])
->setMode('once')
->filterRequest(function ($request) {
// 移除Authorization Header
$request->removeHeader('Authorization');
return $request;
})
->filterResponse(function ($response) {
// 替换响应Body中的信用卡号
$body = $response->getBody();
$body = str_replace('4111111111111111', 'XXXXXXXXXXXX1111', $body);
$response->setBody($body);
return $response;
});
在这个例子中,我们使用filterRequest回调函数移除了请求头中的Authorization,并使用filterResponse回调函数将响应体中的信用卡号替换为了XXXXXXXXXXXX1111。
集成到现有的测试框架
php-vcr/php-vcr 可以很好地集成到 PHPUnit, Codeception 等测试框架中。 上面的例子已经展示了如何与 PHPUnit 集成。
与 Codeception 集成
Codeception 提供了一个 VCR 模块,可以方便地集成 VCR 功能。
-
安装 Codeception VCR 模块:
composer require --dev codeception/module-vcr -
配置
codeception.yml:modules: enabled: - VCR: cassette_path: 'tests/_data/cassettes' mode: 'once' -
在测试用例中使用:
<?php namespace TestsUnit; use TestsSupportUnitTester; class UserProfileTest extends CodeceptionTestUnit { protected UnitTester $tester; public function testGetUserProfile() { $this->tester->haveVCRRecording('get_user_profile'); $client = new GuzzleHttpClient(); $response = $client->get('https://api.example.com/user/123'); $this->assertEquals(200, $response->getStatusCode()); $data = json_decode($response->getBody(), true); $this->assertEquals('John Doe', $data['name']); $this->tester->stopVCRRecording(); } }
在 Codeception 中,使用 $this->tester->haveVCRRecording('cassette_name') 启动 VCR 录制,使用 $this->tester->stopVCRRecording() 停止 VCR 录制。
VCR 的优势总结
使用 VCR 进行测试有很多优势:
- 提高测试速度: 避免了真实的网络请求,测试速度更快。
- 提高测试可靠性: 不受外部 API 稳定性的影响,测试结果更可靠。
- 降低测试成本: 避免了频繁调用外部 API 产生的费用。
- 方便模拟异常情况: 可以通过修改 Cassette 文件来模拟各种异常情况。
- 可以在离线环境下进行测试: 不需要依赖外部 API,可以在任何地方进行测试。
- 保护敏感数据: 可以过滤掉请求和响应中的敏感信息。
一些最佳实践
在使用VCR时,建议遵循以下最佳实践:
- 为每个测试用例创建独立的Cassette: 这样可以避免不同测试用例之间的干扰。
- 保持Cassette文件的小巧: 只记录必要的请求和响应。
- 定期更新Cassette文件: 避免Cassette文件过期。
- 使用
filter_request_callback和filter_response_callback过滤敏感信息: 确保敏感信息不会被泄露。 - 将Cassette文件纳入版本控制: 这样可以方便地跟踪Cassette文件的变化。
- 选择合适的录制模式: 根据实际需求选择
once、rewrite或playback模式。 - 使用描述性的 Cassette 名称: 方便识别 Cassette 文件的作用。
- 在 CI/CD 环境中启用 VCR: 确保每次构建都使用一致的测试环境。
VCR 的局限性
虽然 VCR 带来了很多便利,但也存在一些局限性:
- 需要维护 Cassette 文件: 当外部 API 发生变化时,需要更新 Cassette 文件。
- Cassette 文件可能变得很大: 如果录制了大量的请求和响应,Cassette 文件可能会变得很大。
- 不适用于所有场景: 对于一些需要实时数据的场景,VCR 可能不太适用。
- 可能隐藏了真实的网络问题: 因为测试依赖的是已录制的数据,可能无法及时发现外部 API 的问题。
录制和回放外部API请求,让集成测试更轻松
我们学习了VCR模式的核心概念、配置和使用方法,以及它在实际项目中的应用。通过合理利用VCR,我们可以有效地解决集成测试中的各种问题,提高测试效率和质量,并且降低成本。
使用VCR测试,告别外部依赖的烦恼
总的来说,VCR模式是一种非常有用的技术,可以帮助我们更好地进行集成测试。希望本文能够帮助大家更好地理解和使用VCR。