PHP中的VCR模式测试:录制与回放外部API请求以实现离线集成测试

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会拦截curlstream_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.ymltests/cassettes/failed_payment_invalid_card.yml文件中。

以后再次运行测试用例时,VCR会直接从这些文件中读取响应,而不会真实地调用支付网关的API。

3. 模拟异常情况

通过修改Cassette文件,我们可以模拟各种异常情况,例如:

  • 支付网关服务不可用: 可以将Cassette文件中的响应状态码改为500。
  • 支付金额不足: 可以修改请求中的金额,并修改Cassette文件中的响应内容。
  • 信用卡已过期: 可以修改请求中的信用卡有效期,并修改Cassette文件中的响应内容。

过滤敏感信息

在录制API请求时,我们可能需要过滤掉一些敏感信息,例如API密钥、密码、信用卡号等。 可以使用filter_request_callbackfilter_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 功能。

  1. 安装 Codeception VCR 模块:

    composer require --dev codeception/module-vcr
  2. 配置 codeception.yml:

    modules:
        enabled:
            - VCR:
                cassette_path: 'tests/_data/cassettes'
                mode: 'once'
  3. 在测试用例中使用:

    <?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_callbackfilter_response_callback过滤敏感信息: 确保敏感信息不会被泄露。
  • 将Cassette文件纳入版本控制: 这样可以方便地跟踪Cassette文件的变化。
  • 选择合适的录制模式: 根据实际需求选择oncerewriteplayback模式。
  • 使用描述性的 Cassette 名称: 方便识别 Cassette 文件的作用。
  • 在 CI/CD 环境中启用 VCR: 确保每次构建都使用一致的测试环境。

VCR 的局限性

虽然 VCR 带来了很多便利,但也存在一些局限性:

  • 需要维护 Cassette 文件: 当外部 API 发生变化时,需要更新 Cassette 文件。
  • Cassette 文件可能变得很大: 如果录制了大量的请求和响应,Cassette 文件可能会变得很大。
  • 不适用于所有场景: 对于一些需要实时数据的场景,VCR 可能不太适用。
  • 可能隐藏了真实的网络问题: 因为测试依赖的是已录制的数据,可能无法及时发现外部 API 的问题。

录制和回放外部API请求,让集成测试更轻松

我们学习了VCR模式的核心概念、配置和使用方法,以及它在实际项目中的应用。通过合理利用VCR,我们可以有效地解决集成测试中的各种问题,提高测试效率和质量,并且降低成本。

使用VCR测试,告别外部依赖的烦恼

总的来说,VCR模式是一种非常有用的技术,可以帮助我们更好地进行集成测试。希望本文能够帮助大家更好地理解和使用VCR。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注