単体テストでのHttpClientのモック


110

単体テストで使用するコードをラップしようとすると、いくつか問題があります。問題はこれです。IHttpHandlerインターフェイスがあります。

public interface IHttpHandler
{
    HttpClient client { get; }
}

そしてそれを使用するクラス、HttpHandler:

public class HttpHandler : IHttpHandler
{
    public HttpClient client
    {
        get
        {
            return new HttpClient();
        }
    }
}

次に、simpleIOCを使用してクライアント実装を注入するConnectionクラス:

public class Connection
{
    private IHttpHandler _httpClient;

    public Connection(IHttpHandler httpClient)
    {
        _httpClient = httpClient;
    }
}

そして、私はこのクラスを持つユニットテストプロジェクトを持っています:

private IHttpHandler _httpClient;

[TestMethod]
public void TestMockConnection()
{
    var client = new Connection(_httpClient);

    client.doSomething();  

    // Here I want to somehow create a mock instance of the http client
    // Instead of the real one. How Should I approach this?     

}

これで明らかに、バックエンドからデータ(JSON)を取得するConnectionクラスにメソッドがあります。ただし、このクラスの単体テストを作成したいのですが、実際のバックエンドではなく、あからさまなテストを作成したくありません。私は大成功することなくこれに対する良い答えをグーグルで試みました。以前はMoqを使用してモックすることができますが、httpClientのようなものを使用することはありません。この問題にどのように取り組むべきですか?

前もって感謝します。


1
HttpClientインターフェースでを公開することが問題です。クライアントにHttpClient具象クラスを使用することを強制しています。代わりに、の抽象化を公開する必要がありHttpClientます。
Mike Eason

もう少し詳しく説明してもらえますか?他のクラスでHttpClientの依存関係が必要ないため、接続クラスコンストラクターをどのように構築する必要がありますか?たとえば、接続を使用する他のすべてのクラスがHttpClientに依存するようになるため、接続のコンストラクタで具体的なHttpClientを渡したくないのですか?
tjugg

興味津々で、何をググったの?どうやらmockhttpはいくつかのSEOの改善を使用することができました。
Richard Szalay

@マイク-私の答えで述べたように、HttpClientを抽象化する必要は本当にありません。そのまま完全にテスト可能です。この方法を使用したバックエンドのないテストスイートを持つプロジェクトがたくさんあります。
Richard Szalay 2016

回答:


37

インターフェースは具象HttpClientクラスを公開しているため、このインターフェースを使用するすべてのクラスはそれに関連付けられています。つまり、モックすることはできません。

HttpClientはどのインターフェースからも継承しないため、独自のインターフェースを作成する必要があります。私はデコレータのようなパターンを提案します:

public interface IHttpHandler
{
    HttpResponseMessage Get(string url);
    HttpResponseMessage Post(string url, HttpContent content);
    Task<HttpResponseMessage> GetAsync(string url);
    Task<HttpResponseMessage> PostAsync(string url, HttpContent content);
}

そしてあなたのクラスは次のようになります:

public class HttpClientHandler : IHttpHandler
{
    private HttpClient _client = new HttpClient();

    public HttpResponseMessage Get(string url)
    {
        return GetAsync(url).Result;
    }

    public HttpResponseMessage Post(string url, HttpContent content)
    {
        return PostAsync(url, content).Result;
    }

    public async Task<HttpResponseMessage> GetAsync(string url)
    {
        return await _client.GetAsync(url);
    }

    public async Task<HttpResponseMessage> PostAsync(string url, HttpContent content)
    {
        return await _client.PostAsync(url, content);
    }
}

これらすべてのポイントHttpClientHandlerは、独自のを作成HttpClientすることです。もちろんIHttpHandler、さまざまな方法で実装する複数のクラスを作成することもできます。

このアプローチの主な問題は、あなたが効果的に、しかし、あなたは、そのクラスを作成することができ、単に別のクラスのメソッドを呼び出すクラスを書いているということです継承からHttpClient(参照Nkosiの例では、それは私のものよりもはるかに良いアプローチです)。HttpClientモック可能なインターフェイスがあれば、人生はずっと楽になりますが、残念ながらそうではありません。

ただし、この例はゴールデンチケットではありませんIHttpHandlerは依然として名前空間にHttpResponseMessage属するに依存しているSystem.Net.Httpため、以外の実装が必要な場合はHttpClient、何らかのマッピングを実行して、それらの応答をHttpResponseMessageオブジェクトに変換する必要があります。もちろん、これが唯一の問題であるあなたが複数の実装を使用する必要がある場合のをIHttpHandlerしかし、あなたがそう考える何か、それは世界の終わりではないのですが、それはやるのように、それは見ていません。

とにかく、抽象化されているためIHttpHandler、具象HttpClientクラスを心配することなく、単純にモックできます。

非非同期メソッドをテストすることをお勧めします。非同期メソッドを呼び出しますが、非同期メソッドのユニットテストについて心配する必要がないので、こちらをご覧ください。


これは確かに私の質問に答えます。Nkosisの回答も正しいので、どちらを回答として受け入れればよいかわかりませんが、これを使用します。努力をありがとう
tjugg

@tjuggお役に立ててうれしいです。回答が役に立ったと思われる場合は、自由に投票してください。
Nkosi

3
この回答とNkosiの回答の主な違いは、これがはるかに薄い抽象化であることは注目に値します。薄いものはおそらく控えめなオブジェクトに
Ben Aaronson 2016

227

HttpClientの拡張性はHttpMessageHandler、コンストラクタに渡されることにあります。その目的は、プラットフォーム固有の実装を可能にすることですが、モックすることもできます。HttpClientのデコレータラッパーを作成する必要はありません。

Moqを使用するよりもDSLを使用したい場合は、GitHub / Nugetにライブラリを用意して、少し簡単にします。https//github.com/richardszalay/mockhttp

var mockHttp = new MockHttpMessageHandler();

// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localost/api/user/*")
        .Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON

// Inject the handler or client into your application code
var client = new HttpClient(mockHttp);

var response = await client.GetAsync("http://localhost/api/user/1234");
// or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result;

var json = await response.Content.ReadAsStringAsync();

// No network connection required
Console.Write(json); // {'name' : 'Test McGee'}

1
それで、私はMockHttpMessageHandlerをmessagehandler Httphandlerクラスとして渡すだけですか?または、独自のプロジェクトにどのように実装しましたか
tjugg

2
素晴らしい答えで、最初は知らなかったでしょう。HttpClientでの作業をそれほど悪くしません。
Bealer 2017

6
クライアントのインジェクションに対処したくないが、簡単なテスト性を望んでいる人にとっては、達成するのは簡単です。フィールドを置き換えvar client = new HttpClient()var client = ClientFactory()セットアップするだけinternal static Func<HttpClient> ClientFactory = () => new HttpClient();で、テストレベルでこのフィールドを書き換えることができます。
Chris Marisic

3
@ChrisMarisicは、注入に代わるサービス場所の形式を提案しています。サービスの場所はよく知られているアンチパターンなので、imho注入が望ましいです。
MarioDS 2017年

2
@MarioDSとは関係なく、HttpClient インスタンスをまったく注入しないでください。あなたはこのために建設業者の注入を使用しての死者セットされているなら、あなたは注入しなければならないHttpClientFactoryのようFunc<HttpClient>。HttpClientを依存関係ではなく純粋に実装の詳細と見なしているので、上記で示したように静的を使用します。私は内部を操作するテストで完全に元気です。純粋主義に関心がある場合は、サーバー全体を立ち上げ、ライブコードパスをテストします。あらゆる種類のモックを使用すると、実際の動作ではなく、動作の近似を受け入れることになります。
Chris Marisic

39

最善のアプローチはHttpClientをラップするのではなくHttpMessageHandlerをモックすることであるという他の回答にも同意します。この回答は、HttpClientをインジェクトするという点でユニークであり、シングルトンまたは依存関係のインジェクションで管理できます。

「HttpClientは、一度インスタンス化され、アプリケーションの存続期間を通じて再利用されることを目的としています。」(ソース)。

SendAsyncが保護されているため、HttpMessageHandlerのモックは少し注意が必要です。以下は、xunitとMoqを使用した完全な例です。

using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
using Xunit;
// Use nuget to install xunit and Moq

namespace MockHttpClient {
    class Program {
        static void Main(string[] args) {
            var analyzer = new SiteAnalyzer(Client);
            var size = analyzer.GetContentSize("http://microsoft.com").Result;
            Console.WriteLine($"Size: {size}");
        }

        private static readonly HttpClient Client = new HttpClient(); // Singleton
    }

    public class SiteAnalyzer {
        public SiteAnalyzer(HttpClient httpClient) {
            _httpClient = httpClient;
        }

        public async Task<int> GetContentSize(string uri)
        {
            var response = await _httpClient.GetAsync( uri );
            var content = await response.Content.ReadAsStringAsync();
            return content.Length;
        }

        private readonly HttpClient _httpClient;
    }

    public class SiteAnalyzerTests {
        [Fact]
        public async void GetContentSizeReturnsCorrectLength() {
            // Arrange
            const string testContent = "test content";
            var mockMessageHandler = new Mock<HttpMessageHandler>();
            mockMessageHandler.Protected()
                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
                .ReturnsAsync(new HttpResponseMessage {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StringContent(testContent)
                });
            var underTest = new SiteAnalyzer(new HttpClient(mockMessageHandler.Object));

            // Act
            var result = await underTest.GetContentSize("http://anyurl");

            // Assert
            Assert.Equal(testContent.Length, result);
        }
    }
}

1
私はこれが本当に好きだった。mockMessageHandler.Protected()キラーでした。この例をありがとう。ソースをまったく変更せずにテストを記述できます。
タイリオン2017年

1
参考までに、Moq 4.8は保護されたメンバーの強く型付けされたモッキングをサポートしています-github.com/Moq/moq4/wiki/Quickstart
Richard Szalay

2
これは素晴らしいですね。また、MoqはReturnAsyncをサポートしているため、コードは次のようになります.ReturnsAsync(new HttpResponseMessage {StatusCode = HttpStatusCode.OK, Content = new StringContent(testContent)})
kord

@kordに感謝します。それを答えに追加しました
PointZeroTwo '29

3
「SandAsync」がいくつかのパラメーターで呼び出されたことを確認する方法はありますか?... Protected()。Verify(...)を使用しようとしましたが、非同期メソッドでは機能しないようです。
Rroman、2018年

29

これはよくある質問であり、私はHttpClientをモックする機能を強く望んでいましたが、HttpClientをモックするべきではないということに気付いたと思います。そうするのは理にかなっているように思えますが、私たちはオープンソースライブラリで目にするものに洗脳されてきたと思います。

コードをモックした「クライアント」がよくあるので、個別にテストできるので、自動的に同じ原理をHttpClientに適用しようとします。HttpClientは実際に多くのことを行います。それをHttpMessageHandlerのマネージャーと考えることができるので、それをあざける必要はありません。そのため、インターフェースがまだありません。単体テストやサービスの設計で本当に関心があるのは、HttpMessageHandlerです。これは、応答を返すものであり、それモックすることができるためです。

また、おそらくHttpClientをより大きな取引のように扱い始める必要があることも指摘する価値があります。例:新しいHttpClientの初期化を最小限に抑えます。それらを再利用してください。それらは再利用されるように設計されており、使用した場合に必要なリソースがはるかに少なくなります。それをより大きな取引のように扱い始めると、それをモックしたいと思うとはるかに間違っているように感じ、今ではメッセージハンドラーがクライアントではなく注入しているものになります。

つまり、クライアントではなくハンドラーの周囲の依存関係を設計します。さらに良いのは、ハンドラーを注入できるHttpClientを使用し、代わりにそれを注入可能な依存関係として使用する抽象的な「サービス」です。次に、テストでハンドラーを偽造して、テストをセットアップするための応答を制御できます。

HttpClientのラッピングは非常に時間の無駄です。

更新:Joshua Doomsの例を参照してください。それはまさに私がお勧めするものです。


17

また、あなたがする必要がコメントで述べたように抽象化離れてHttpClient、それに結合されないようにします。私は過去に似たようなことをしたことがあります。私があなたがやろうとしていることで私がやったことを適応させようとします。

最初にHttpClientクラスを見て、必要な機能を提供するかどうかを決定しました。

ここに可能性があります:

public interface IHttpClient {
    System.Threading.Tasks.Task<T> DeleteAsync<T>(string uri) where T : class;
    System.Threading.Tasks.Task<T> DeleteAsync<T>(Uri uri) where T : class;
    System.Threading.Tasks.Task<T> GetAsync<T>(string uri) where T : class;
    System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class;
    System.Threading.Tasks.Task<T> PostAsync<T>(string uri, object package);
    System.Threading.Tasks.Task<T> PostAsync<T>(Uri uri, object package);
    System.Threading.Tasks.Task<T> PutAsync<T>(string uri, object package);
    System.Threading.Tasks.Task<T> PutAsync<T>(Uri uri, object package);
}

再び前に述べたように、これは特定の目的のためでした。ほとんどの依存関係を処理するものに完全に抽象化しHttpClient、返して欲しいものに焦点を当てました。を抽象化しHttpClientて、必要な機能のみを提供する方法を評価する必要があります。

これにより、テストに必要なものだけを模擬できるようになります。

IHttpHandler完全に廃止してHttpClient抽象化を使用することをお勧めしIHttpClientます。しかし、ハンドラーインターフェースの本体を抽象化されたクライアントのメンバーで置き換えることができるため、単に選択しているわけではありません。

次に、の実装をIHttpClient使用して、実際の/コンクリートHttpClientまたはその他のオブジェクトをラップ/適応させることができますHttpClient。これは、特に要求された機能を提供するサービスであるHTTPリクエストを作成するために使用できます。抽象化の使用はクリーン(私の意見)であり、SOLIDアプローチであり、フレームワークの変更に伴い、基になるクライアントを他のものに切り替える必要がある場合、コードをより保守しやすくすることができます。

以下は、実装方法のスニペットです。

/// <summary>
/// HTTP Client adaptor wraps a <see cref="System.Net.Http.HttpClient"/> 
/// that contains a reference to <see cref="ConfigurableMessageHandler"/>
/// </summary>
public sealed class HttpClientAdaptor : IHttpClient {
    HttpClient httpClient;

    public HttpClientAdaptor(IHttpClientFactory httpClientFactory) {
        httpClient = httpClientFactory.CreateHttpClient(**Custom configurations**);
    }

    //...other code

     /// <summary>
    ///  Send a GET request to the specified Uri as an asynchronous operation.
    /// </summary>
    /// <typeparam name="T">Response type</typeparam>
    /// <param name="uri">The Uri the request is sent to</param>
    /// <returns></returns>
    public async System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class {
        var result = default(T);
        //Try to get content as T
        try {
            //send request and get the response
            var response = await httpClient.GetAsync(uri).ConfigureAwait(false);
            //if there is content in response to deserialize
            if (response.Content.Headers.ContentLength.GetValueOrDefault() > 0) {
                //get the content
                string responseBodyAsText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                //desrialize it
                result = deserializeJsonToObject<T>(responseBodyAsText);
            }
        } catch (Exception ex) {
            Log.Error(ex);
        }
        return result;
    }

    //...other code
}

上記の例でわかるように、通常使用に関連する多くの重労働HttpClientは、抽象化の背後に隠されています。

接続クラスは、抽象化されたクライアントで注入できます

public class Connection
{
    private IHttpClient _httpClient;

    public Connection(IHttpClient httpClient)
    {
        _httpClient = httpClient;
    }
}

あなたのテストはあなたのSUTに必要なものを模倣できます

private IHttpClient _httpClient;

[TestMethod]
public void TestMockConnection()
{
    SomeModelObject model = new SomeModelObject();
    var httpClientMock = new Mock<IHttpClient>();
    httpClientMock.Setup(c => c.GetAsync<SomeModelObject>(It.IsAny<string>()))
        .Returns(() => Task.FromResult(model));

    _httpClient = httpClientMock.Object;

    var client = new Connection(_httpClient);

    // Assuming doSomething uses the client to make
    // a request for a model of type SomeModelObject
    client.doSomething();  
}

これが答えです。上記の抽象化HttpClientと、を使用して特定のインスタンスを作成するためのアダプターHttpClientFactory。これを行うと、ここでの目標であるHTTPリクエストを超えたロジックのテストが簡単になります。
pimbrouwers

13

他の回答に基づいて、私はこのコードをお勧めします。このコードには外部の依存関係はありません。

[TestClass]
public class MyTestClass
{
    [TestMethod]
    public async Task MyTestMethod()
    {
        var httpClient = new HttpClient(new MockHttpMessageHandler());

        var content = await httpClient.GetStringAsync("http://some.fake.url");

        Assert.AreEqual("Content as string", content);
    }
}

public class MockHttpMessageHandler : HttpMessageHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("Content as string")
        };

        return await Task.FromResult(responseMessage);
    }
}

4
あなたは効果的にモックをテストしています。モックの本当の力は、各テストで期待値を設定し、その動作を変更できることです。HttpMessageHandler自分で実装する必要があるという事実は、それをほぼ不可能にします-そして、メソッドがそうであるからprotected internalです。
MarioDS 2017年

3
@MarioDSポイントは、HTTP応答をモックして残りのコードをテストできることです。HttpClientを取得するファクトリを注入する場合、テストではこのHttpClientを提供できます。
chris313​​89

13

問題は、少し逆さまになっていることだと思います。

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

    public AuroraClient() : this(new HttpClientHandler())
    {
    }

    public AuroraClient(HttpMessageHandler messageHandler)
    {
        _client = new HttpClient(messageHandler);
    }
}

上記のクラスを見ると、これがあなたの望んでいることだと思います。マイクロソフトでは、最適なパフォーマンスを得るためにクライアントを存続させておくことをお勧めします。そのため、このタイプの構造でそれを実現できます。また、HttpMessageHandlerは抽象クラスであるため、モック可能です。テストメソッドは次のようになります。

[TestMethod]
public void TestMethod1()
{
    // Arrange
    var mockMessageHandler = new Mock<HttpMessageHandler>();
    // Set up your mock behavior here
    var auroraClient = new AuroraClient(mockMessageHandler.Object);
    // Act
    // Assert
}

これにより、HttpClientの動作を模擬しながらロジックをテストできます。

申し訳ありませんが、これを書いて自分で試したところ、HttpMessageHandlerの保護されたメソッドをモックできないことに気付きました。その後、適切なモックを挿入できるように次のコードを追加しました。

public interface IMockHttpMessageHandler
{
    Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
}

public class MockHttpMessageHandler : HttpMessageHandler
{
    private readonly IMockHttpMessageHandler _realMockHandler;

    public MockHttpMessageHandler(IMockHttpMessageHandler realMockHandler)
    {
        _realMockHandler = realMockHandler;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return await _realMockHandler.SendAsync(request, cancellationToken);
    }
}

これで書かれたテストは次のようになります。

[TestMethod]
public async Task GetProductsReturnsDeserializedXmlXopData()
{
    // Arrange
    var mockMessageHandler = new Mock<IMockHttpMessageHandler>();
    // Set up Mock behavior here.
    var client = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object));
    // Act
    // Assert
}

9

私の同僚の1人は、ほとんどのHttpClientメソッドがすべて内部SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)で呼び出されることに気づきました。これは、以下の仮想メソッドですHttpMessageInvoker

したがって、モックアウトする最も簡単な方法HttpClientは、単にその特定のメソッドをモックすることでした。

var mockClient = new Mock<HttpClient>();
mockClient.Setup(client => client.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>())).ReturnsAsync(_mockResponse.Object);

そして、コードは、HttpClientクラスメソッドのほとんど(すべてではない)を呼び出すことができます。

httpClient.SendAsync(req)

ここをチェックして、https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.csを確認して ください


1
SendAsync(HttpRequestMessage)ただし、これは直接呼び出すコードでは機能しません。この便利な関数を使用しないようにコードを変更できる場合SendAsyncは、オーバーライドによって直接HttpClientをモックすることが、実際に私が見つけた最もクリーンなソリューションです。
ディランニコルソン

8

1つの代替策は、リクエストURLに一致するパターンに基づいて返信定型文を返すスタブHTTPサーバーをセットアップすることです。つまり、モックではなく実際のHTTPリクエストをテストします。歴史的にこれはかなりの開発努力を要し、単体テストで検討するのがはるかに遅くなりましたが、OSSライブラリWireMock.netは使いやすく、多くのテストで実行できるほど高速であるため、検討する価値があります。セットアップは数行のコードです:

var server = FluentMockServer.Start();
server.Given(
      Request.Create()
      .WithPath("/some/thing").UsingGet()
   )
   .RespondWith(
       Response.Create()
       .WithStatusCode(200)
       .WithHeader("Content-Type", "application/json")
       .WithBody("{'attr':'value'}")
   );

テストでのWiremockの使用に関する詳細とガイダンスは、ここにあります。


8

これは私にとってうまくいった簡単な解決策です。

moqモッキングライブラリの使用。

// ARRANGE
var handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
handlerMock
   .Protected()
   // Setup the PROTECTED method to mock
   .Setup<Task<HttpResponseMessage>>(
      "SendAsync",
      ItExpr.IsAny<HttpRequestMessage>(),
      ItExpr.IsAny<CancellationToken>()
   )
   // prepare the expected response of the mocked http call
   .ReturnsAsync(new HttpResponseMessage()
   {
      StatusCode = HttpStatusCode.OK,
      Content = new StringContent("[{'id':1,'value':'1'}]"),
   })
   .Verifiable();

// use real http client with mocked handler here
var httpClient = new HttpClient(handlerMock.Object)
{
   BaseAddress = new Uri("http://test.com/"),
};

var subjectUnderTest = new MyTestClass(httpClient);

// ACT
var result = await subjectUnderTest
   .GetSomethingRemoteAsync('api/test/whatever');

// ASSERT
result.Should().NotBeNull(); // this is fluent assertions here...
result.Id.Should().Be(1);

// also check the 'http' call was like we expected it
var expectedUri = new Uri("http://test.com/api/test/whatever");

handlerMock.Protected().Verify(
   "SendAsync",
   Times.Exactly(1), // we expected a single external request
   ItExpr.Is<HttpRequestMessage>(req =>
      req.Method == HttpMethod.Get  // we expected a GET request
      && req.RequestUri == expectedUri // to this uri
   ),
   ItExpr.IsAny<CancellationToken>()
);

出典:https : //gingter.org/2018/07/26/how-to-mock-httpclient-in-your-net-c-unit-tests/


私もこれをうまく使いました。私はこれがまだnoohterのnuget依存関係をデフラグするよりも好きで、実際にはフードの下で何が起こっているのかについて少し学ぶことができます。いいことに、ほとんどの方法はSendAsyncとにかく使用するので、追加の設定は必要ありません。
スティーブペティファー

4

私は多くの答えに確信が持てません。

まず、を使用するメソッドを単体テストしたいとしますHttpClientHttpClient実装で直接インスタンス化しないでください。のインスタンスを提供する責任を工場に注入する必要HttpClientがあります。そうすれば、後でそのファクトリーをモックして、必要なものを返すことができHttpClientます(例:HttpClient本物のモックではなく)。

したがって、次のようなファクトリがあります。

public interface IHttpClientFactory
{
    HttpClient Create();
}

そして実装:

public class HttpClientFactory
    : IHttpClientFactory
{
    public HttpClient Create()
    {
        var httpClient = new HttpClient();
        return httpClient;
    }
}

もちろん、この実装をIoCコンテナに登録する必要があります。Autofacを使用する場合は、次のようになります。

builder
    .RegisterType<IHttpClientFactory>()
    .As<HttpClientFactory>()
    .SingleInstance();

これで、適切でテスト可能な実装ができます。あなたの方法が次のようなものであると想像してください:

public class MyHttpClient
    : IMyHttpClient
{
    private readonly IHttpClientFactory _httpClientFactory;

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

    public async Task<string> PostAsync(Uri uri, string content)
    {
        using (var client = _httpClientFactory.Create())
        {
            var clientAddress = uri.GetLeftPart(UriPartial.Authority);
            client.BaseAddress = new Uri(clientAddress);
            var content = new StringContent(content, Encoding.UTF8, "application/json");
            var uriAbsolutePath = uri.AbsolutePath;
            var response = await client.PostAsync(uriAbsolutePath, content);
            var responseJson = response.Content.ReadAsStringAsync().Result;
            return responseJson;
        }
    }
}

次にテスト部分です。HttpClient拡張HttpMessageHandler抽象的です。HttpMessageHandlerデリゲートを受け入れる"モック"を作成して、モックを使用するときに各テストの各動作をセットアップできるようにします。

public class MockHttpMessageHandler 
    : HttpMessageHandler
{
    private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc;

    public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc)
    {
        _sendAsyncFunc = sendAsyncFunc;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return await _sendAsyncFunc.Invoke(request, cancellationToken);
    }
}

そして今、Moq(およびユニットテストを読みやすくするライブラリであるFluentAssertions)の助けを借りて、以下を使用するメソッドPostAsyncをユニットテストするために必要なすべてのものがあります。 HttpClient

public static class PostAsyncTests
{
    public class Given_A_Uri_And_A_JsonMessage_When_Posting_Async
        : Given_WhenAsync_Then_Test
    {
        private SalesOrderHttpClient _sut;
        private Uri _uri;
        private string _content;
        private string _expectedResult;
        private string _result;

        protected override void Given()
        {
            _uri = new Uri("http://test.com/api/resources");
            _content = "{\"foo\": \"bar\"}";
            _expectedResult = "{\"result\": \"ok\"}";

            var httpClientFactoryMock = new Mock<IHttpClientFactory>();
            var messageHandlerMock =
                new MockHttpMessageHandler((request, cancellation) =>
                {
                    var responseMessage =
                        new HttpResponseMessage(HttpStatusCode.Created)
                        {
                            Content = new StringContent("{\"result\": \"ok\"}")
                        };

                    var result = Task.FromResult(responseMessage);
                    return result;
                });

            var httpClient = new HttpClient(messageHandlerMock);
            httpClientFactoryMock
                .Setup(x => x.Create())
                .Returns(httpClient);

            var httpClientFactory = httpClientFactoryMock.Object;

            _sut = new SalesOrderHttpClient(httpClientFactory);
        }

        protected override async Task WhenAsync()
        {
            _result = await _sut.PostAsync(_uri, _content);
        }


        [Fact]
        public void Then_It_Should_Return_A_Valid_JsonMessage()
        {
            _result.Should().BeEquivalentTo(_expectedResult);
        }
    }
}

明らかに、このテストはばかげています。私たちは実際にモックをテストしています。しかし、あなたはアイデアを得ます。次のような実装に応じて、意味のあるロジックをテストする必要があります。

  • 応答のコードステータスが201でない場合、例外をスローする必要がありますか?
  • 応答テキストを解析できない場合、どうなりますか?

この回答の目的は、HttpClientを使用するものをテストすることでした。これは、これを行うための優れたクリーンな方法です。


4

少し遅れてパーティーに参加しますが、ワイヤーモッキング(https://github.com/WireMock-Net/WireMock.Net)を使用するのが好きですが、ダウンストリームREST依存関係とのドットネットコアマイクロサービスの統合では、可能な限り、)です。

IHttpClientFactoryを拡張するTestHttpClientFactoryを実装することで、メソッドをオーバーライドできます

HttpClient CreateClient(文字列名)

そのため、アプリ内で名前付きクライアントを使用すると、ワイヤードモックにワイヤーされたHttpClientを返すことができます。

このアプローチの良い点は、テストしているアプリケーション内で何も変更せず、サービスへの実際のRESTリクエストを実行し、実際のダウンストリームリクエストが返すjson(またはその他)をモックするコース統合テストを有効にすることです。これにより、テストが簡潔になり、アプリケーションのモックが最小限になります。

    public class TestHttpClientFactory : IHttpClientFactory 
{
    public HttpClient CreateClient(string name)
    {
        var httpClient = new HttpClient
        {
            BaseAddress = new Uri(G.Config.Get<string>($"App:Endpoints:{name}"))
            // G.Config is our singleton config access, so the endpoint 
            // to the running wiremock is used in the test
        };
        return httpClient;
    }
}

そして

// in bootstrap of your Microservice
IHttpClientFactory factory = new TestHttpClientFactory();
container.Register<IHttpClientFactory>(factory);

2

メソッドをHttpClient使用SendAsyncしてすべてを実行するため、メソッドを実行してモックするHTTP Requestsことができますoverride SendAsyncHttpClient

そのラップを作成HttpClientするにはinterface、以下のようなもの

public interface IServiceHelper
{
    HttpClient GetClient();
}

次にinterface、サービスでの依存性注入に上記を使用し、以下のサンプル

public class SampleService
{
    private readonly IServiceHelper serviceHelper;

    public SampleService(IServiceHelper serviceHelper)
    {
        this.serviceHelper = serviceHelper;
    }

    public async Task<HttpResponseMessage> Get(int dummyParam)
    {
        try
        {
            var dummyUrl = "http://www.dummyurl.com/api/controller/" + dummyParam;
            var client = serviceHelper.GetClient();
            HttpResponseMessage response = await client.GetAsync(dummyUrl);               

            return response;
        }
        catch (Exception)
        {
            // log.
            throw;
        }
    }
}

単体テストプロジェクトで、モック用のヘルパークラスを作成しSendAsyncます。ここでは、メソッドをオーバーライドするオプションを提供するFakeHttpResponseHandlerクラスです。上書きした後、セットアップにそれぞれの応答方法の必要性を呼びかけている作成し、そのための方法、となどととしてそうあるたびという場合と一致するように構成を返しますが。inheriting DelegatingHandlerSendAsyncSendAsyncHTTP RequestSendAsyncDictionarykeyUrivalueHttpResponseMessageHTTP RequestUriSendAsyncHttpResponseMessage

public class FakeHttpResponseHandler : DelegatingHandler
{
    private readonly IDictionary<Uri, HttpResponseMessage> fakeServiceResponse;
    private readonly JavaScriptSerializer javaScriptSerializer;
    public FakeHttpResponseHandler()
    {
        fakeServiceResponse =  new Dictionary<Uri, HttpResponseMessage>();
        javaScriptSerializer =  new JavaScriptSerializer();
    }

    /// <summary>
    /// Used for adding fake httpResponseMessage for the httpClient operation.
    /// </summary>
    /// <typeparam name="TQueryStringParameter"> query string parameter </typeparam>
    /// <param name="uri">Service end point URL.</param>
    /// <param name="httpResponseMessage"> Response expected when the service called.</param>
    public void AddFakeServiceResponse(Uri uri, HttpResponseMessage httpResponseMessage)
    {
        fakeServiceResponse.Remove(uri);
        fakeServiceResponse.Add(uri, httpResponseMessage);
    }

    /// <summary>
    /// Used for adding fake httpResponseMessage for the httpClient operation having query string parameter.
    /// </summary>
    /// <typeparam name="TQueryStringParameter"> query string parameter </typeparam>
    /// <param name="uri">Service end point URL.</param>
    /// <param name="httpResponseMessage"> Response expected when the service called.</param>
    /// <param name="requestParameter">Query string parameter.</param>
    public void AddFakeServiceResponse<TQueryStringParameter>(Uri uri, HttpResponseMessage httpResponseMessage, TQueryStringParameter requestParameter)
    {
        var serilizedQueryStringParameter = javaScriptSerializer.Serialize(requestParameter);
        var actualUri = new Uri(string.Concat(uri, serilizedQueryStringParameter));
        fakeServiceResponse.Remove(actualUri);
        fakeServiceResponse.Add(actualUri, httpResponseMessage);
    }

    // all method in HttpClient call use SendAsync method internally so we are overriding that method here.
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if(fakeServiceResponse.ContainsKey(request.RequestUri))
        {
            return Task.FromResult(fakeServiceResponse[request.RequestUri]);
        }

        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
        {
            RequestMessage = request,
            Content = new StringContent("Not matching fake found")
        });
    }
}

IServiceHelperフレームワークなどをモックすることで、新しい実装を作成します。このFakeServiceHelperクラスを使用してFakeHttpResponseHandlerクラスを挿入できるためHttpClient、これによって作成されるたびに、実際の実装の代わりにclass使用FakeHttpResponseHandler classされます。

public class FakeServiceHelper : IServiceHelper
{
    private readonly DelegatingHandler delegatingHandler;

    public FakeServiceHelper(DelegatingHandler delegatingHandler)
    {
        this.delegatingHandler = delegatingHandler;
    }

    public HttpClient GetClient()
    {
        return new HttpClient(delegatingHandler);
    }
}

テストでFakeHttpResponseHandler classは、Uriとを追加して構成しますHttpResponseMessageUri実際あるべきserviceエンドポイントUri時になるようにoverridden SendAsyncする方法は、実際から呼び出されservice、実装、それが一致するUriにはDictionary構成されてと応答HttpResponseMessage。構成後FakeHttpResponseHandler object、偽のIServiceHelper実装に挿入します。次に、FakeServiceHelper classを実際のサービスに挿入します。これにより、実際のサービスがoverride SendAsyncメソッドを使用するようになります。

[TestClass]
public class SampleServiceTest
{
    private FakeHttpResponseHandler fakeHttpResponseHandler;

    [TestInitialize]
    public void Initialize()
    {
        fakeHttpResponseHandler = new FakeHttpResponseHandler();
    }

    [TestMethod]
    public async Task GetMethodShouldReturnFakeResponse()
    {
        Uri uri = new Uri("http://www.dummyurl.com/api/controller/");
        const int dummyParam = 123456;
        const string expectdBody = "Expected Response";

        var expectedHttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(expectdBody)
        };

        fakeHttpResponseHandler.AddFakeServiceResponse(uri, expectedHttpResponseMessage, dummyParam);

        var fakeServiceHelper = new FakeServiceHelper(fakeHttpResponseHandler);

        var sut = new SampleService(fakeServiceHelper);

        var response = await sut.Get(dummyParam);

        var responseBody = await response.Content.ReadAsStringAsync();

        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
        Assert.AreEqual(expectdBody, responseBody);
    }
}

GitHubリンク:サンプル実装があります


このコードは問題を解決する可能性がありますが、これが問題を解決する方法と理由の説明含めると、投稿の品質が向上し、投票数が増える可能性があります。あなたが今尋ねている人だけでなく、将来の読者のための質問に答えていることを忘れないでください。回答を編集して説明を追加し、適用される制限と前提を示してください。
БогданОпир

フィードバックの更新された説明をありがとう@БогданОпир。
ghosh-arun

1

あなたはRichardSzalay MockHttpを使うことができますHttpMessageHandlerをモックし、テスト中に使用するHttpClientオブジェクトを返すライブラリを。

GitHub MockHttp

PM>インストールパッケージRichardSzalay.MockHttp

GitHubドキュメントから

MockHttpは、HttpClientを駆動するエンジンである代替のHttpMessageHandlerを定義します。これは、流れるような構成APIを提供し、返信定型文を提供します。呼び出し元(アプリケーションのサービス層など)は、その存在を認識しません。

GitHubの例

 var mockHttp = new MockHttpMessageHandler();

// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localhost/api/user/*")
        .Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON

// Inject the handler or client into your application code
var client = mockHttp.ToHttpClient();

var response = await client.GetAsync("http://localhost/api/user/1234");
// or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result;

var json = await response.Content.ReadAsStringAsync();

// No network connection required
Console.Write(json); // {'name' : 'Test McGee'}

1

これは古い質問ですが、ここでは見られなかった解決策を使って回答を拡張したいという気持ちがあります。
Microsoftを偽装して(System.Net.Http)、テスト中にShinsContextを使用できます。

  1. VS 2017で、System.Net.Httpアセンブリを右クリックし、[Add Fakes Assembly]を選択します
  2. 単体テストメソッドのShimsContext.Create()を使用してコードを配置します。このようにして、HttpClientの偽造を計画しているコードを分離できます。
  3. 実装とテストによって異なりますが、HttpClientでメソッドを呼び出して戻り値を偽造したい場合は、必要なすべての動作を実装することをお勧めします。ShimHttpClient.AllInstancesを使用すると、テスト中に作成されたすべてのインスタンスで実装が偽装されます。たとえば、GetAsync()メソッドを偽装する場合は、次のようにします。

    [TestMethod]
    public void FakeHttpClient()
    {
        using (ShimsContext.Create())
        {
            System.Net.Http.Fakes.ShimHttpClient.AllInstances.GetAsyncString = (c, requestUri) =>
            {
              //Return a service unavailable response
              var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.ServiceUnavailable);
              var task = Task.FromResult(httpResponseMessage);
              return task;
            };
    
            //your implementation will use the fake method(s) automatically
            var client = new Connection(_httpClient);
            client.doSomething(); 
        }
    }

1

私はDI環境にいたので、非常に単純なことをしました。

public class HttpHelper : IHttpHelper
{
    private ILogHelper _logHelper;

    public HttpHelper(ILogHelper logHelper)
    {
        _logHelper = logHelper;
    }

    public virtual async Task<HttpResponseMessage> GetAsync(string uri, Dictionary<string, string> headers = null)
    {
        HttpResponseMessage response;
        using (var client = new HttpClient())
        {
            if (headers != null)
            {
                foreach (var h in headers)
                {
                    client.DefaultRequestHeaders.Add(h.Key, h.Value);
                }
            }
            response = await client.GetAsync(uri);
        }

        return response;
    }

    public async Task<T> GetAsync<T>(string uri, Dictionary<string, string> headers = null)
    {
        ...

        rawResponse = await GetAsync(uri, headers);

        ...
    }

}

そしてモックは:

    [TestInitialize]
    public void Initialize()
    {
       ...
        _httpHelper = new Mock<HttpHelper>(_logHelper.Object) { CallBase = true };
       ...
    }

    [TestMethod]
    public async Task SuccessStatusCode_WithAuthHeader()
    {
        ...

        _httpHelper.Setup(m => m.GetAsync(_uri, myHeaders)).Returns(
            Task<HttpResponseMessage>.Factory.StartNew(() =>
            {
                return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
                {
                    Content = new StringContent(JsonConvert.SerializeObject(_testData))
                };
            })
        );
        var result = await _httpHelper.Object.GetAsync<TestDTO>(...);

        Assert.AreEqual(...);
    }

1

必要なのは、ctorにHttpMessageHandler渡すクラスのテストバージョンだけですHttpClient。重要な点は、テストHttpMessageHandlerクラスにはHttpRequestHandler、呼び出し元が設定して簡単に処理できるデリゲートがあるというHttpRequestことです。

public class FakeHttpMessageHandler : HttpMessageHandler
    {
        public Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> HttpRequestHandler { get; set; } =
        (r, c) => 
            new HttpResponseMessage
            {
                ReasonPhrase = r.RequestUri.AbsoluteUri,
                StatusCode = HttpStatusCode.OK
            };


        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            return Task.FromResult(HttpRequestHandler(request, cancellationToken));
        }
    }

このクラスのインスタンスを使用して、具体的なHttpClientインスタンスを作成できます。HttpRequestHandlerデリゲートを介して、HttpClientからの発信HTTPリクエストを完全に制御できます。


1

PointZeroTwoの回答に触発されて、NUnitFakeItEasyを使用したサンプルを次に示します。

SystemUnderTest この例では、テストしたいクラスです-サンプルのコンテンツはありませんが、すでにそれがあると思います!

[TestFixture]
public class HttpClientTests
{
    private ISystemUnderTest _systemUnderTest;
    private HttpMessageHandler _mockMessageHandler;

    [SetUp]
    public void Setup()
    {
        _mockMessageHandler = A.Fake<HttpMessageHandler>();
        var httpClient = new HttpClient(_mockMessageHandler);

        _systemUnderTest = new SystemUnderTest(httpClient);
    }

    [Test]
    public void HttpError()
    {
        // Arrange
        A.CallTo(_mockMessageHandler)
            .Where(x => x.Method.Name == "SendAsync")
            .WithReturnType<Task<HttpResponseMessage>>()
            .Returns(Task.FromResult(new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.InternalServerError,
                Content = new StringContent("abcd")
            }));

        // Act
        var result = _systemUnderTest.DoSomething();

        // Assert
        // Assert.AreEqual(...);
    }
}

「x.Method.Name」に対して言及されている、メソッドにパラメーターを渡したい場合はどうなりますか?
Shailesh

0

おそらく、現在のプロジェクトで変更するコードがあるかもしれませんが、新しいプロジェクトの場合は、Flurlの使用を絶対に検討する必要があります。

https://flurl.dev

これは、.NET用のHTTPクライアントライブラリであり、滑らかなインターフェイスを備えており、それを使用してHTTPリクエストを行うコードのテスト機能を特に有効にします。

ウェブサイトにはたくさんのコードサンプルがありますが、簡単に言うと、コードでこのように使用します。

使用法を追加します。

using Flurl;
using Flurl.Http;

getリクエストを送信し、レスポンスを読み取ります。

public async Task SendGetRequest()
{
   var response = await "https://example.com".GetAsync();
   // ...
}

単体テストでは、Flurlはモックとして機能し、必要に応じて動作するように構成したり、実行された呼び出しを検証したりできます。

using (var httpTest = new HttpTest())
{
   // Arrange
   httpTest.RespondWith("OK", 200);

   // Act
   await sut.SendGetRequest();

   // Assert
   httpTest.ShouldHaveCalled("https://example.com")
      .WithVerb(HttpMethod.Get);
}

0

慎重に検索した後、これを達成するための最良のアプローチを見つけました。

    private HttpResponseMessage response;

    [SetUp]
    public void Setup()
    {
        var handlerMock = new Mock<HttpMessageHandler>();

        handlerMock
           .Protected()
           .Setup<Task<HttpResponseMessage>>(
              "SendAsync",
              ItExpr.IsAny<HttpRequestMessage>(),
              ItExpr.IsAny<CancellationToken>())
           // This line will let you to change the response in each test method
           .ReturnsAsync(() => response);

        _httpClient = new HttpClient(handlerMock.Object);

        yourClinet = new YourClient( _httpClient);
    }

お気づきのとおり、私はMoqおよびMoq.Protectedパッケージを使用しました。


0

私の2セントを追加します。特定のhttp要求メソッドをGetまたはPostのいずれかで模擬するには。これでうまくいきました。

mockHttpMessageHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(a => a.Method == HttpMethod.Get), ItExpr.IsAny<CancellationToken>())
                                                .Returns(Task.FromResult(new HttpResponseMessage()
                                                {
                                                    StatusCode = HttpStatusCode.OK,
                                                    Content = new StringContent(""),
                                                })).Verifiable();
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.