Skip to content

Latest commit

 

History

History
296 lines (215 loc) · 15 KB

README.ja.md

File metadata and controls

296 lines (215 loc) · 15 KB

Moq.Contrib.HttpClient

NuGet ci build badge tested on badge

English

MoqでHttpClientとIHttpClientFactoryのテストダブルを作るための拡張メソッドです

かつて、HttpClientをモックすることが驚くほど難しかって、解決方法はHttpClientそのものをモックする代わりにラッパーを作ること、あるいは他のHTTPライブラリで完全に置き換えることでした。このパッケージはHTTPリクエストのモックをサービスメソッドと同じように簡単にする拡張メソッドをMoqに付加する

インストール

Install-Package Moq.Contrib.HttpClient

または dotnet add package Moq.Contrib.HttpClient

API

Moqの普通のメソッドにリクエスト版とレスポンス版が追加される:

  • Setup → SetupRequest, SetupAnyRequest
  • SetupSequence → SetupRequestSequence, SetupAnyRequestSequence
  • Verify → VerifyRequest, VerifyAnyRequest
  • Returns(Async) → ReturnsResponse, ReturnsJsonResponse

リクエスト

リクエストのヘルパーはすべて同じオーバーロードがある:

SetupAnyRequest()
SetupRequest([HttpMethod method, ]Predicate<HttpRequestMessage> match)
SetupRequest(string|Uri requestUrl[, Predicate<HttpRequestMessage> match])
SetupRequest(HttpMethod method, string|Uri requestUrl[, Predicate<HttpRequestMessage> match])

requestUrlは正確なURLをマッチして、match述語はクエリパラメータやヘッダーでマッチ出来てリクエストボディをチェックするためにasyncになれる

レスポンス

レスポンスのヘルパーはStringContent、JsonContent(System.Text.Jsonによる)、ByteArrayContent、StreamContent、それともステータスコードだけを送ることを簡単にする:

ReturnsResponse(HttpStatusCode statusCode[, HttpContent content], Action<HttpResponseMessage> configure = null)
ReturnsResponse([HttpStatusCode statusCode, ]string content, string mediaType = null, Encoding encoding = null, Action<HttpResponseMessage> configure = null))
ReturnsResponse([HttpStatusCode statusCode, ]byte[]|Stream content, string mediaType = null, Action<HttpResponseMessage> configure = null)
ReturnsJsonResponse<T>([HttpStatusCode statusCode, ]T value, JsonSerializerOptions options = null, Action<HttpResponseMessage> configure = null)

statusCodeが省略されると200 OKにディフォルトする。configureアクションがレスポンスのヘッダーを設定するように使える

用例

一般的な使用法

// HttpClientで送ったリクエストがモックされるハンドラのSendAsync()中に通る
var handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var client = handler.CreateClient();

// すべてのリクエストに404を送る簡単な例え
handler.SetupAnyRequest()
    .ReturnsResponse(HttpStatusCode.NotFound);

// JSONを返すエンドポイントへのGETリクエストをマッチする (200 OKにデフォルトする)
handler.SetupRequest(HttpMethod.Get, "https://example.com/api/stuff")
    .ReturnsJsonResponse(model);

// 任意なconfigureアクションでレスポンスにもっとのヘッダーを設定する
handler.SetupRequest(HttpMethod.Get, "https://example.com/api/stuff")
    .ReturnsResponse(bytes, configure: response =>
    {
        response.Content.Headers.LastModified = new DateTime(2022, 3, 9);
    });
💡 なぜHttpClientのためにMockBehavior.Strictを使うべき

以下の点を考慮します:

handler.SetupRequest(HttpMethod.Get, "https://example.com/api/foos")
    .ReturnsJsonResponse(expected);

List<Foo> actual = await foosService.GetFoos();

actual.Should().BeEquivalentTo(expected);

このテストは以下の例外で予期せず失敗する:

System.InvalidOperationException : Handler did not return a response message.

なぜならMoqはセットアップがマッチしない場合既定値を返すLooseモードがデフォルトですが、HttpClientはハンドラからnullを受け取るとInvalidOperationExceptionをスローする

MockBehavior.Strictに変更したら:

- var handler = new Mock<HttpMessageHandler>();
+ var handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);

もっと便利な例外をもらって、送ったリクエストも付いてる (ここ、URLがfoosではなくfooとミスされた):

Moq.MockException : HttpMessageHandler.SendAsync(Method: GET, RequestUri: 'https://example.com/api/foo', Version: 1.1, Content: <null>, Headers:
{
}, System.Threading.CancellationToken) invocation failed with mock behavior Strict.
All invocations on the mock must have a corresponding setup.

クエリパラメータやヘッダーやJSONボディでリクエストをマッチする

// もっと複雑にマッチするための述語を受け取れる
handler.SetupRequest(r => r.Headers.Authorization?.Parameter != authToken)
    .ReturnsResponse(HttpStatusCode.Unauthorized);

// 述語がボディをチェックするためにasyncにもなれる
handler
    .SetupRequest(HttpMethod.Post, url, async request =>
    {
        // このセットアップは予期されるIDのあるリクエストのみをマッチする
        var json = await request.Content.ReadFromJsonAsync<Model>();
        return json.Id == expected.Id;
    })
    .ReturnsResponse(HttpStatusCode.Created);

// とくにはクエリパラメータのあるURLをマッチしたら便利です
handler
    .SetupRequest(r =>
    {
        Url url = r.RequestUri;
        return url.Path == baseUrl.AppendPathSegment("endpoint") &&
            url.QueryParams["hoge"].Equals("piyo");
    })
    .ReturnsResponse("stuff");

最後のはクエリ文字列のチェックに役立つFlurlというURLビルダーのライブラリを使う

JSONリクエストのいろいろな確認方法など、詳しい説明はリクエスト拡張のテストのMatchesCustomPredicateとMatchesQueryParametersを見てください

リクエストのシークエンスをセットアップする

Moqは2種類のシークエンスがある:

  1. SetupSequence() は順に値を返す一つセットアップを作る
  2. InSequence().Setup() は必ず順にマッチするように複数のセットアップをWhen()条件で作る

両方がサポートされるけど、サービスメソッドのように、普段は普通のセットアップが最適です。互いに独立しているリクエスト(つまり、前リクエストに返された情報に依存していない)が特定の順序でマッチする必要がある場合は後者が便利です

用例はシークエンス拡張のテストを見てください

リクエストボディに基づいてレスポンスを書く

もっと複雑のレスポンスのためには普通のReturnsがリクエストのヘルパーと一緒に使える:

handler.SetupRequest("https://example.com/hello")
    .Returns(async (HttpRequestMessage request, CancellationToken _) => new HttpResponseMessage()
    {
        Content = new StringContent($"こんにちは、{await request.Content.ReadAsStringAsync()}")
    });

var response = await client.PostAsync("https://example.com/hello", new StringContent("世界"));
var body = await response.Content.ReadAsStringAsync(); // こんにちは、世界

IHttpClientFactoryの使い方

概要

HttpClientはIDisposableなのでusing中によく入れられてしまうが、直感に反して、これは間違うしソケットを使い果たしていることにつながる可能性があるんです。一般の忠告は単一のHttpClientを再利用することですが、そうしたらDNSの変更に反応しない

ASP.NET Coreが「HttpClientのライフタイムを手動で管理するときに発生する一般的なDNSの問題を回避のために基礎となるHttpClientMessageHandlerインスタンスのプーリングとライフタイムを管理する」IHttpClientFactoryを導入する。ボーナスとして、これがミドルウェアをプラグインするHttpClientの能力をもっととっつきやすいにする。例えば、再試行と失敗を自動的に処理するためにPollyを使うこと

ファクトリーをモックする方法

クラスが単にIHttpClientFactoryによって注入したHttpClientを受け取ると、特に何もする必要がない。コンストラクタがファクトリーそのものを受け取る場合は、同じようにモックできる:

var handler = new Mock<HttpMessageHandler>();
var factory = handler.CreateClientFactory();

このファクトリーがクラスに渡されたりAutoMockerによって注入されたりできる。factory.CreateClient()を呼び出すコードがモックなハンドラを使うクライアントを受ける

名前付きクライアント

CreateClientFactory()という拡張メソッドはディフォルトのクライエントを返すようにセットアップされたモックを返す。名前付きクライアントを使っている場合は次のようにセットアップを追加できる:

// 名前付きクライアントも設定する(デフォルトを無効にする)
Mock.Get(factory).Setup(x => x.CreateClient("api"))
    .Returns(() =>
    {
        var client = handler.CreateClient();
        client.BaseAddress = ApiBaseUrl;
        return client;
    });

※「Extension methods (here: HttpClientFactoryExtensions.CreateClient) may not be used in setup / verification expressions.」というエラーが出たら、上に"api"がある場所に文字列を渡しているのを確認してください

統合テスト

統合テストは、サービスコレクションにあるIHttpClientFactory実装を変えるよりも、既存のDIインフラを活用してプライマリとしてモックなハンドラを使うように設定できる:

public class ExampleTests : IClassFixture<WebApplicationFactory<Startup>>
{
    private readonly WebApplicationFactory<Startup> factory;
    private readonly Mock<HttpMessageHandler> githubHandler = new();

    public ExampleTests(WebApplicationFactory<Startup> factory)
    {
        this.factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                // デフォルト(名前なし)のクライアントの場合は`Options.DefaultName`とを使って
                services.AddHttpClient("github")
                    .ConfigurePrimaryHttpMessageHandler(() => githubHandler.Object);
            });
        });
    }

これで、統合テストは本番環境と同じConfigureServices()(それともProgram.cs)での依存性注入とHttpClient設定を使う

実例は、このASP.NET Coreのサンプルのアプリその統合テストを見てください

より詳細な例

このライブラリのユニットテストがヘルパーやさまざまなユースケースの例として役立つように書かれた:

ライセンス

MIT