web-dev-qa-db-ja.com

特定のHttpMessageHandlerを使用した単一インスタンスHttpClientの注入

私が取り組んでいる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を保持するのではなく、シングルトンライフサイクルを使用してコンストラクタインジェクションを介してインジェクトでき​​るようにすることです。単体テスト中にHttpClientHttpMessageHandlerを追加して、静的フィールドでHttpClientsをインスタンス化するHttpClient Factoryクラスを使用することを考えました。その後、HttpClientファクトリをサービスクラスに挿入することで取得でき、関連するHttpMessageHandlerを使用した別の実装を単体テストで返すことができます。しかし、上記のどれも特に清潔感はなく、より良い方法があるに違いないと感じていますか?

ご質問は、私に知らせてください。

15
Chris Lawrence

コメントから会話に追加すると、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依存関係を抽象化できます。

サービスの利用者は、データの取得方法を正確に知る必要はありません。

18
Nkosi

複雑だと思います。必要なのは、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
...
12
Tseng

私の現在の好みはターゲットエンドポイントドメインごとに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構成が別のサービスに漏洩するのを防ぎます。

3
alastairtree

私はパーティーに遅れるかもしれませんが、ユニットテストで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);
}
1
Pure.Krome