私が取り組んでいるASP.Net Coreプロジェクトの一環として、WebApi内からいくつかの異なるRESTベースのAPIエンドポイントと通信する必要があります。これを実現するために、それぞれが静的HttpClient
をインスタンス化するサービスクラスをいくつか使用しています。基本的に、WebApiが接続するRESTベースのエンドポイントごとにサービスクラスがあります。
各サービスクラスで静的HttpClient
がインスタンス化される方法の例を以下に示します。
private static HttpClient _client = new HttpClient()
{
BaseAddress = new Uri("http://endpointurlexample"),
};
上記はうまく機能していますが、HttpClient
を使用しているサービスクラスの効果的な単体テストは許可されていません。単体テストを実行できるようにするために、単体テストでHttpMessageHandler
に使用したい偽のHttpClient
がありますが、HttpClient
は上記のようにインスタンス化されます単体テストの一部として、偽のHttpMessageHandler
を適用できません。
サービスクラスのHttpClient
がアプリケーション全体で1つのインスタンス(エンドポイントごとに1つのインスタンス)のままで、単体テスト中に異なるHttpMessageHandler
を適用できるようにする最良の方法は何ですか?
私が考えていた1つのアプローチは、静的フィールドを使用してサービスクラスでHttpClient
を保持するのではなく、シングルトンライフサイクルを使用してコンストラクタインジェクションを介してインジェクトできるようにすることです。単体テスト中にHttpClient
にHttpMessageHandler
を追加して、静的フィールドでHttpClient
sをインスタンス化するHttpClient
Factoryクラスを使用することを考えました。その後、HttpClient
ファクトリをサービスクラスに挿入することで取得でき、関連するHttpMessageHandler
を使用した別の実装を単体テストで返すことができます。しかし、上記のどれも特に清潔感はなく、より良い方法があるに違いないと感じていますか?
ご質問は、私に知らせてください。
コメントから会話に追加すると、HttpClient
ファクトリが必要になるようです
public interface IHttpClientFactory {
HttpClient Create(string endpoint);
}
コア機能の実装は次のようになります。
static IDictionary<string, HttpClient> cache = new Dictionary<string, HttpClient>();
public HttpClient Create(string endpoint) {
HttpClient client = null;
if(cache.TryGetValue(endpoint, out client)) {
return client;
}
client = new HttpClient {
BaseAddress = new Uri(endpoint),
};
cache[endpoint] = client;
return client;
}
そうは言っても、上記の設計に特に満足していないなら。クライアントが実装の詳細にならないように、サービスの背後にあるHttpClient
依存関係を抽象化できます。
サービスの利用者は、データの取得方法を正確に知る必要はありません。
複雑だと思います。必要なのは、HttpClientファクトリまたはHttpClient
プロパティを持つアクセッサで、ASP.NET CoreがHttpContext
の挿入を許可するのと同じ方法で使用します
public interface IHttpClientAccessor
{
HttpClient Client { get; }
}
public class DefaultHttpClientAccessor : IHttpClientAccessor
{
public HttpClient Client { get; }
public DefaultHttpClientAccessor()
{
Client = new HttpClient();
}
}
これをサービスに注入します
public class MyRestClient : IRestClient
{
private readonly HttpClient client;
public MyRestClient(IHttpClientAccessor httpClientAccessor)
{
client = httpClientAccessor.Client;
}
}
startup.csへの登録:
services.AddSingleton<IHttpClientAccessor, DefaultHttpClientAccessor>();
単体テストの場合は、モックするだけです
// Moq-esque
// Arrange
var httpClientAccessor = new Mock<IHttpClientAccessor>();
var httpHandler = new HttpMessageHandler(..) { ... };
var httpContext = new HttpContext(httpHandler);
httpClientAccessor.SetupGet(a => a.Client).Returns(httpContext);
// Act
var restClient = new MyRestClient(httpClientAccessor.Object);
var result = await restClient.GetSomethingAsync(...);
// Assert
...
私の現在の好みはターゲットエンドポイントドメインごとに1回HttpClient
から派生で、HttpClient
を直接使用するのではなく、依存性注入を使用してシングルトンにします。
Example.comにHTTPリクエストを送信しているとすると、ExampleHttpClient
を継承し、HttpClient
と同じコンストラクタシグネチャを持つHttpClient
を使用して、 HttpMessageHandler
通常どおり。
public class ExampleHttpClient : HttpClient
{
public ExampleHttpClient(HttpMessageHandler handler) : base(handler)
{
BaseAddress = new Uri("http://example.com");
// set default headers here: content type, authentication, etc
}
}
次に、依存性注入登録でExampleHttpClient
をシングルトンとして設定し、HttpMessageHandler
の登録を一時として追加します。これは、httpクライアントタイプごとに1回だけ作成されるためです。このパターンを使用すると、HttpClient
またはスマートファクトリに複数の複雑な登録を行って、宛先ホスト名に基づいてそれらを構築する必要がありません。
Example.comと通信する必要があるものはすべて、ExampleHttpClient
のコンストラクター依存関係を取得し、すべてが同じインスタンスを共有し、設計どおりに接続プーリングを取得します。
また、この方法により、デフォルトのヘッダー、コンテンツタイプ、認証、ベースアドレスなどを配置するためのより良い場所が提供され、あるサービスのHTTP構成が別のサービスに漏洩するのを防ぎます。
私はパーティーに遅れるかもしれませんが、ユニットテストでHttpClientエンドポイントをテストできるようにするヘルパーnugetパッケージを作成しました。
NuGet:install-package WorldDomination.HttpClient.Helpers
レポ: https://github.com/PureKrome/HttpClient.Helpers
基本的な考え方は、you偽の応答ペイロードを作成し、そのFakeHttpMessageHandler
インスタンスをコードに渡すことです。ペイロード。次に、コードが実際にそのURIエンドポイントをヒットしようとすると、...ではなく、代わりに偽の応答を返します。魔法!
そして、これは本当に簡単な例です:
[Fact]
public async Task GivenSomeValidHttpRequests_GetSomeDataAsync_ReturnsAFoo()
{
// Arrange.
// Fake response.
const string responseData = "{ \"Id\":69, \"Name\":\"Jane\" }";
var messageResponse = FakeHttpMessageHandler.GetStringHttpResponseMessage(responseData);
// Prepare our 'options' with all of the above fake stuff.
var options = new HttpMessageOptions
{
RequestUri = MyService.GetFooEndPoint,
HttpResponseMessage = messageResponse
};
// 3. Use the fake response if that url is attempted.
var messageHandler = new FakeHttpMessageHandler(options);
var myService = new MyService(messageHandler);
// Act.
// NOTE: network traffic will not leave your computer because you've faked the response, above.
var result = await myService.GetSomeFooDataAsync();
// Assert.
result.Id.ShouldBe(69); // Returned from GetSomeFooDataAsync.
result.Baa.ShouldBeNull();
options.NumberOfTimesCalled.ShouldBe(1);
}