MoqでHttpClientとIHttpClientFactoryのテストダブルを作るための拡張メソッドです
かつて、HttpClientをモックすることが驚くほど難しかって、解決方法はHttpClientそのものをモックする代わりにラッパーを作ること、あるいは他のHTTPライブラリで完全に置き換えることでした。このパッケージはHTTPリクエストのモックをサービスメソッドと同じように簡単にする拡張メソッドをMoqに付加する
Install-Package Moq.Contrib.HttpClient
または dotnet add package Moq.Contrib.HttpClient
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.
// もっと複雑にマッチするための述語を受け取れる
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種類のシークエンスがある:
SetupSequence()
は順に値を返す一つセットアップを作る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(); // こんにちは、世界
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のサンプルのアプリとその統合テストを見てください
このライブラリのユニットテストがヘルパーやさまざまなユースケースの例として役立つように書かれた:
- リクエスト拡張のテスト — SetupとVerifyのヘルパーに焦点をあてて、リクエストをマッチするいろいろな方法を説明する
- レスポンス拡張のテスト — ReturnsResponse(とReturnsJsonResponse)のオーバーロードに焦点をあてます
- シークエンス拡張のテスト — 明示的なシークエンスをモックすることを実証する
MIT