HttpClientFactoryをテストする方法


いつでも働いているとき.NETアプリケーション、あなたが見る最も一般的なものの1つは依存性注入を使用してIHttpClientFactory サービスのコンストラクタへのインスタンス.もちろん、そのサービスをテストする必要があります.良い単体テストを書くためには、依存性を模擬して行動を完全に制御するのが良い習慣です.mock依存性へのよく知られたライブラリはmoqですそれを統合することはとても簡単ですIMyService , あなたはそれを使用してそれの塊を作成することができますMock<IMyService> .
しかし、ここに問題がある:モッキングIHttpClientFactory それは単純ではありませんMock<IHttpClientFactory> 十分ではない.
この記事では、我々はモックの方法を学びますIHttpClientFactory 依存、どのようにHTTPの呼び出しのための動作を定義するには、最後に、我々は深く依存してモンクを許可するモスクの高度な機能に飛び込む.レッツゴー!

問題の紹介
完全に問題を理解するためには、具体的な例が必要です.
次のクラスは、入力文字列を指定して、削除HTTP呼び出しを使用してリモートクライアントに送信するメソッドを使用してサービスを実装します.
public class MyExternalService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public MyExternalService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task DeleteObject(string objectName)
    {
        string path = $"/objects?name={objectName}";
        var client = _httpClientFactory.CreateClient("ext_service");

        var httpResponse = await client.DeleteAsync(path);

        httpResponse.EnsureSuccessStatusCode();
    }
}
注目すべき重要な点は、私たちがIHttpClientFactory ; 我々はまた、新しいを作成しているHttpClient 使用するたびにそれが必要です_httpClientFactory.CreateClient("ext_service") .
ご存知のように、新しいインスタンスを作成してはいけませんHttpClient ソケットの消耗の危険性を避けるために毎回オブジェクトを参照するlinks below ).
このアプローチには大きな問題があります.あなたは、単にmockIHttpClientFactory 依存関係ですが、手動でHttpClient そして、その内臓を追跡する.
もちろん、我々は本物を使用しませんIHttpClientFactory インスタンス:私たちは実際のHTTP呼び出しを実行するアプリケーションを望まない.依存関係をモックする必要があります.
映画スタントダブルスとして嘲笑依存性を考えてください:あなたは、アクションシーンを実行しながら、あなたのメインスターを傷つけるためにしたくない.同様に、テストを実行するときにアプリケーションが実際の操作を実行する必要はありません.

私たちはメソッドをテストするためにmoqを使用し、HTTP呼び出しが正しくobjectName クエリ文字列内の変数.

IHttpClientFactoryのモックをMOQでつくる方法
モンクの作成のための完全なコードから始めましょうIHttpClientFactory s
var handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);

HttpResponseMessage result = new HttpResponseMessage();

handlerMock
    .Protected()
    .Setup<Task<HttpResponseMessage>>(
        "SendAsync",
        ItExpr.IsAny<HttpRequestMessage>(),
        ItExpr.IsAny<CancellationToken>()
    )
    .ReturnsAsync(result)
    .Verifiable();

var httpClient = new HttpClient(handlerMock.Object) { 
        BaseAddress = new Uri("https://www.code4it.dev/") 
    };

var mockHttpClientFactory = new Mock<IHttpClientFactory>();

mockHttpClientFactory.Setup(_ => _.CreateClient("ext_service")).Returns(httpClient);

service = new MyExternalService(mockHttpClientFactory.Object);
たくさんのものが進んでいますね.
完全にすべてのこれらのステートメントが意味するものを理解するためにそれを壊しましょう.

HttpMessageHandlerの操作
我々が会う最初の命令は
var handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
それはどういう意味ですか.HttpMessageHandler すべてのHTTPリクエストの基本的な部分です.指定されたエンドポイントにsendasync呼び出しを行い、HttpRequestMessage パラメータとして渡されるオブジェクトです.
我々は何が起こるかに興味があるのでHttpMessageHandler , 我々はそれをモックアップし、結果を変数に格納する必要があります.
注意MockBehavior.Strict ? これはオプションのパラメータであり、対応する設定がない場合には例外をスローします.試してみると、その引数をコンストラクタに削除し、handlerMock.Setup() Part :テストを実行すると、型のエラーが表示されますMoq.MockException .
次のステップ:モックドの動作を定義するHttpMessageHandler
HttpMessageHandlerの動作定義
今、我々は使用するときに何が起こるかを定義する必要がありますhandlerMock HTTP操作中のオブジェクト:
HttpResponseMessage result = new HttpResponseMessage();

handlerMock
    .Protected()
    .Setup<Task<HttpResponseMessage>>(
        "SendAsync",
        ItExpr.IsAny<HttpRequestMessage>(),
        ItExpr.IsAny<CancellationToken>()
    )
    .ReturnsAsync(result)
    .Verifiable();
私たちが出会う最初のことはProtected() . なぜ?
我々がなぜそれを必要とするか、そして、次の操作の意味が何であるかを完全に理解するために、我々はHttpMessageHandler :
// Summary: A base type for HTTP message handlers.
public abstract class HttpMessageHandler : IDisposable
{
    /// Other stuff here...

    // Summary: Send an HTTP request as an asynchronous operation.
    protected internal abstract Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, 
        CancellationToken cancellationToken);
}
この断片から、私たちは、我々に方法があるのを見ることができます.SendAsync , はHttpRequestMessage オブジェクトとCancellationToken , HTTPリクエストを扱うものです.しかし、このメソッドは保護されています.したがって、私たちはProtected() の保護されたメソッドにアクセスするにはHttpMessageHandler クラス名とパラメータを使用して設定しなければなりませんSetup メソッド.

次の2つの詳細に注意してください.
  • この名前を文字列として設定する方法を指定します
  • 我々がパラメータの実際の値を気にしないと言うために、我々は使用しますItExpr の代わりにIt 我々は保護されたメンバーのセットアップを扱っているので.
  • sendasyncがpublicなメソッドであれば、以下のようにしました.
    handlerMock
        .Setup(_ => _.SendAsync(
            It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()) 
        );
    
    しかし、それが保護された方法であるので、我々は前にリストした方法を使う必要があります.
    次に、SendAsync 型のオブジェクトを返すHttpResponseMessage : ここでは、応答の内容については気にしないでください.そうすれば、私たちは、更なるカスタマイズなしでこのようにそれを残すことができます.

    HttpClientの作成
    今のところ、我々はHttpMessageHandler オブジェクト、我々はそれを渡すことができますHttpClient の新しいインスタンスを作成するコンストラクタHttpClient それは我々が必要とする行為.
    var httpClient = new HttpClient(handlerMock.Object) { 
            BaseAddress = new Uri("https://www.code4it.dev/") 
        };
    
    ここでは、値を設定しましたBaseAddress HTTPコールを実行する際にnull参照を避ける有効なURIに対するプロパティ.非既存のURLを使用することができます:重要なことは、URLがよく形成される必要がありますです.

    IHttpClientFactoryインスタンスの設定
    我々は最終的に作成する準備が整いましたIHttpClientFactory !
    var mockHttpClientFactory = new Mock<IHttpClientFactory>();
    
    mockHttpClientFactory.Setup(_ => _.CreateClient("ext_service")).Returns(httpClient);
    
    var service = new MyExternalService(mockHttpClientFactory.Object);
    
    だから、私たちのモックを作成しますIHttpClientFactory のインスタンスを定義するHttpClient を返します.CreateClient("ext_service") . 最後に、我々はIHttpClientFactory のコンストラクタにMyExternalService .

    IHttpClientFactoryによって実行される呼び出しを確認する方法
    今、我々のテストでは、テスト中の操作を行ったと仮定します.
    // setup IHttpClientFactory
    await service.DeleteObject("my-name");
    
    どうすれば我々はHttpClient 実際にクエリ文字列の"my name "との終点と呼ばれますか?前と同様に、コード全体を見てみましょう.
    // verify that the query string contains "my-name"
    
    handlerMock.Protected()
     .Verify(
        "SendAsync",
        Times.Exactly(1), // we expected a single external request
        ItExpr.Is<HttpRequestMessage>(req => 
            req.RequestUri.Query.Contains("my-name")// Query string contains my-name
        ),
        ItExpr.IsAny<CancellationToken>()
        );
    

    プロテクトインスタンスへのアクセス
    既に見たように、HTTP操作を実行するオブジェクトはHttpMessageHandler , ここで私たちはmockkedし、格納されているhandlerMock 変数.
    それから、私たちは、何が起こったかを確認する必要がありますSendAsync メソッドです.保護メソッドですしたがって、私たちはProtected そのメンバにアクセスする.

    クエリ文字列のチェック
    我々の主張の中心部分はこうです.
    ItExpr.Is<HttpRequestMessage>(req => 
        req.RequestUri.Query.Contains("my-name")// Query string contains my-name
    ),
    
    再び、我々はprotected メンバーなので、使用する必要がありますItExpr の代わりにIt .
    The Is<HttpRequestMessage> メソッドは関数を受け入れますFunc<HttpRequestMessage, bool> これは、HttpRequestMessage テストの下で-私たちのケースではreq - 指定した述語にマッチします.もしそうならば、テストは通ります.

    コードのリファクタリング
    あなたのクラスのすべてのテストメソッドのためにそのコードを繰り返す必要があると想像してください.
    だから我々はそれをリファクタリングすることができますHttpMessageHandler へのモックSetUp メソッド:
    [SetUp]
    public void Setup()
    {
        this.handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
    
        HttpResponseMessage result = new HttpResponseMessage();
    
        this.handlerMock
        .Protected()
        .Setup<Task<HttpResponseMessage>>(
            "SendAsync",
            ItExpr.IsAny<HttpRequestMessage>(),
            ItExpr.IsAny<CancellationToken>()
        )
        .Returns(Task.FromResult(result))
        .Verifiable()
        ;
    
        var httpClient = new HttpClient(handlerMock.Object) { 
            BaseAddress = new Uri("https://www.code4it.dev/") 
            };
    
        var mockHttpClientFactory = new Mock<IHttpClientFactory>();
    
        mockHttpClientFactory.Setup(_ => _.CreateClient("ext_service")).Returns(httpClient);
    
        this.service = new MyExternalService(mockHttpClientFactory.Object);
    }
    
    そして、handlerMock and service 一部の個人的なメンバーで.
    次に、アサーション部分を別のメソッドに移動できます.
    public static void Verify(this Mock<HttpMessageHandler> mock, Func<HttpRequestMessage, bool> match)
    {
        mock.Protected().Verify(
            "SendAsync",
            Times.Exactly(1), // we expected a single external request
            ItExpr.Is<HttpRequestMessage>(req => match(req)
            ),
            ItExpr.IsAny<CancellationToken>()
        );
    }
    
    それで、我々のテストがちょうど一束の線に単純化されることができます:
    [Test]
    public async Task Method_Should_ReturnSomething_When_Condition()
    {
        //Arrange occurs in the SetUp phase
    
        //Act
        await service.DeleteObject("my-name");
    
        //Assert
        handlerMock.Verify(r => r.RequestUri.Query.Contains("my-name"));
    }
    

    更なる読書
    🔗 Why we need HttpClientFactory | Microsoft Docs
    🔗 HttpMessageHandler class | Microsoft Docs
    🔗 Mock objects with static, complex data by using Manifest resources | Code4IT
    🔗 Moq documentation | GitHub
    🔗 How you can create extension methods in C# | Code4IT

    ラッピング
    この記事では、依存しているサービスをテストするためにどれだけトリッキーになるかを見ましたIHttpClientFactory インスタンス.幸運にも、我々は依存関係をモックするためにmoqのようなツールに頼ることができて、それらの依存のふるまいを完全に制御します.
    モッキングIHttpClientFactory 難しい.しかし、ここで我々は、それらの難しさを克服し、我々のテストを書くのが簡単で理解する方法を見つけました.
    そこにたくさんのnugetパッケージがあります.あなたのお気に入りは何ですか?
    ハッピーコーディング!
    🐧