cspray/http-client-test-interceptor

此包最新版本(dev-main)没有可用的许可信息。

dev-main 2024-05-04 12:59 UTC

This package is auto-updated.

Last update: 2024-09-04 13:35:18 UTC


README

这是一个用于测试使用它引入的应用拦截器设计模式的 amphp/http-client v5 框架。使用此库的主要原因是您的单元测试或集成测试中测试已知的 HTTP 交互。此库提供了 2 种测试这些交互的机制

  1. 编写 单元测试,其中每个请求都应该是模拟的,并且不应进行任何网络请求。
  2. 编写 集成测试,如果未存储该请求的固定文件,则请求将通过网络发送。从网络请求收到的响应将被存储,后续请求不会通过网络发送。

无论您编写单元测试还是集成测试,都使用相同的底层机制。您可以指定请求的细粒度部分,以便与它们进行匹配,或提供自己的匹配请求与固定文件的策略。

有许多不使用此库的理由

  • 您没有使用 amphp/http-client v5 或更高版本。
  • 您已经有了测试 API 交互的可行、有效的解决方案。

安装

composer require amphp/http-client:v5.0.0-beta.3
composer require --dev cspray/http-client-test-interceptor

amphp/http-client 库仍在测试版。您可能需要调整您的最低稳定性,直到 http-client 发布 5.0 版本。

单元测试快速入门

单元测试旨在测试您的 HttpClient 对特定、已知的 HTTP 交互的响应。例如,您可能需要测试您的测试代码对不同头部、特定错误代码或精确的正文结构的响应。这些测试通常很难进行,甚至可能无法通过集成测试可靠地进行。

与编写使用固定文件的集成测试不同,没有必要的设置步骤。第一步是编写您的 TestCase!

<?php declare(strict_types=1);

namespace Acme\HttpMockingDemo;

use Amp\Http\Client\HttpClientBuilder;use Amp\Http\Client\Request;use Cspray\HttpClientTestInterceptor\HttpMock\MockResponse;use Cspray\HttpClientTestInterceptor\HttpMockAwareTestTrait;use League\Uri\Http;use PHPUnit\Framework\TestCase;

final class ApiUnitTest extends TestCase {

    use HttpMockAwareTestTrait;
    
    public function testGetResource() : void {
        $request = new Request(Http::createFromString('http://example.com'), 'POST');
        $response = MockResponse::fromBody('My expected body');
        $this->httpMock()->whenClientReceivesRequest($request)->willReturnResponse($response);
            
        $client = (new HttpClientBuilder())->intercept($this->getMockingInterceptor())->build();
        
        // We match on the values of the mocked Request ... NOT the identity of the object
        $actual = $client->request(new Request(Http::createFromString('http://example.com')), 'POST');
        
        // If you were to uncomment the below line this test would fail with a RequestNotMocked exception as the methods do not match
        // $client->request(new Request(Http::createFromString('http://example.com')), 'GET');
        
        self::assertSame($response, $actual);
    }

}

模拟请求匹配

由于模拟需要更多的手动设置,因此它们匹配的方式使用一组更宽松的请求匹配策略;而不是检查整个请求,只需将方法和 URI 匹配即可。如果您想使这更严格,可以在定义要模拟的请求时提供一组匹配器。

<?php declare(strict_types=1);

namespace Acme\HttpMockingDemo;

use Amp\Http\Client\HttpClientBuilder;use Amp\Http\Client\Request;use Cspray\HttpClientTestInterceptor\HttpMock\MockResponse;use Cspray\HttpClientTestInterceptor\HttpMockAwareTestTrait;use Cspray\HttpClientTestInterceptor\Matcher\Matcher;use League\Uri\Http;use PHPUnit\Framework\TestCase;

final class ApiHeadersMatchingUnitTest extends TestCase {

    use HttpMockAwareTestTrait;
    
    public function testGetResource() : void {
        $request = new Request(Http::createFromString('http://example.com'), 'POST');
        $request->setHeader('Authorization', 'some-token');
        $response = MockResponse::fromBody('My expected body');
        $this->httpMock()->whenClientReceivesRequest($request, [Matcher::Uri, Matcher::Method, Matcher::Headers])
            ->willReturnResponse($response);
            
        $client = (new HttpClientBuilder())->intercept($this->getMockingInterceptor())->build();
        
        // We match on the values of the mocked Request ... NOT the identity of the object
        $actualRequest = new Request('http://example.com', 'POST');
        $actualRequest->setHeader('Authorization', 'some-token');
        $actual = $client->request($actualRequest);
        
        // If you were to uncomment the below line this test would fail with a RequestNotMocked exception as the _headers_ do not match
        // $client->request(new Request(Http::createFromString('http://example.com')), 'POST');
        
        self::assertSame($response, $actual);
    }

}

集成测试快速入门

集成测试旨在测试您的 HttpClient 对 实际 API 的真实响应。当请求第一次通过网络发送时,会存储一个固定文件与响应。如果再次发送匹配的请求,将根据存储的固定文件生成响应,而不是通过网络发送。

创建固定文件目录

首先需要做的是创建一个存储 HTTP 固定文件的目录。没有“正确”的存储这些文件的位置,任何可写目录都适用。假设您的测试位于名为 test 的目录中,本指南建议创建一个 test/http_fixture 目录。

mkdir -p test/http_fixture

标记发送 HTTP 请求的测试

现在,在您的测试中,将 TestCase 或测试方法标记为存储和用于匹配请求的固定文件路径。

<?php declare(strict_types=1);

// Stored in test/ApiTest.php

namespace Acme\HttpFixtureDemo;

use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use Cspray\HttpClientTestInterceptor\Attribute\HttpFixture;
use Cspray\HttpClientTestInterceptor\HttpFixtureAwareTestTrait;
use PHPUnit\Framework\TestCase;

#[HttpFixture(__DIR__ . '/http_fixture')]
final class ApiTest extends TestCase {

    use HttpFixtureAwareTestTrait;

    public function testGetResource() : void {
        $httpClient = (new HttpClientBuilder())
            ->intercept($this->getTestInterceptor())
            ->build();
        
        $response = $httpClient->request(new Request('https://api.example.com'));
        
        self::assertSame(200, $response->getStatus());
    }

}

集成请求匹配

默认情况下,库将尝试将请求的每个方面与集成测试中存储的固定文件进行匹配。可以精确控制哪些请求部分进行匹配。例如,让我们设置我们的 TestInterceptor,只匹配请求方法和 URI,而不是所有内容。

<?php declare(strict_types=1);

// Stored in test/ApiTest.php

namespace Acme\HttpFixtureDemo;

use Amp\Http\Client\HttpClientBuilder;use Amp\Http\Client\Request;use Cspray\HttpClientTestInterceptor\Attribute\HttpFixture;use Cspray\HttpClientTestInterceptor\Attribute\HttpRequestMatchers;use Cspray\HttpClientTestInterceptor\HttpFixtureAwareTestTrait;use Cspray\HttpClientTestInterceptor\Matcher\Matcher;use PHPUnit\Framework\TestCase;

#[HttpFixture(__DIR__ . '/http_fixture')]
#[HttpRequestMatchers(Matcher::Method, Matcher::Uri)]
final class ApiTest extends TestCase {

    use HttpFixtureAwareTestTrait;

    public function testGetResource() : void {
        $httpClient = (new HttpClientBuilder())
            ->intercept($this->getTestInterceptor())
            ->build();
        
        $response = $httpClient->request(new Request('https://api.example.com'));
        
        self::assertSame(200, $response->getStatus());
    }

}

现在使用的 RequestMatchingStrategy 仅检查方法和 URI。您可以根据请求的适当部分来组合您的请求匹配。还可以在单独的测试方法上定义 #[HttpRequestMatchers]#[HttpFixture] 属性。如果您在测试方法上提供了属性,则这些属性将覆盖 TestCase 上的属性。