web-dev-qa-db-ja.com

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

コードをラップして単体テストで使用しようとすると、いくつかの問題があります。問題はこれです。 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のようなものは使用しませんでした。この問題にどのように取り組むべきですか?

前もって感謝します。

82
tjugg

インターフェースは具象HttpClientクラスを公開するため、このインターフェースを使用するクラスはすべてそれに結び付けられます。これは、モック化できないことを意味します。

HttpClientはどのインターフェースからも継承しないため、独自に作成する必要があります。 decorator-likeパターンをお勧めします:

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からinheritsを作成することができます(Nkosiの例を参照してください、それは私のものよりもはるかに良いアプローチです)。 HttpClientにモックできるインターフェースがあれば、人生はずっと楽になりますが、残念ながらそうではありません。

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

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

non-asyncメソッドをテストすることをお勧めします。これらのメソッドは非同期メソッドを呼び出しますが、非同期メソッドの単体テストについて心配する必要はありません。 ここ

30
Mike Eason

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'}
179
Richard Szalay

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);
        }
    }
}
32
PointZeroTwo

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

コードをモックする「クライアント」をよく見かけますので、単独でテストできるため、HttpClientに同じ原則を自動的に適用しようとします。 HttpClientは実際に多くのことを行います。 HttpMessageHandlerのマネージャーと考えることができるので、それをモックしたくはありません。だからこそstillにはインターフェースがありません。単体テストやサービスの設計で本当に興味がある部分は、HttpMessageHandlerです。なぜなら、それが応答を返すので、あなたはcanをモックするからです。

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

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

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

更新:Joshua Doomsの例を参照してください。まさに私が推奨しているものです。

25
Sinaesthetic

コメントでも述べたように、-[abstractHttpClientを結合しないようにする必要があります。私は過去に似たようなことをしました。私はあなたがやろうとしていることで私がやったことを適応させようとします。

最初に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またはその他のオブジェクトをラップ/適応することができます。これは、本当に必要なサービスとしてHTTPリクエストを行うために使用できます。 HttpClientに具体的に並置された機能を提供していました。抽象化の使用はクリーン(私の意見)で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();  
}
14
Nkosi

他の答えに基づいて、外部の依存関係がないこのコードをお勧めします:

[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);
    }
}
11
pius

問題は、あなたがそれを少し逆さまにしていることだと思います。

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 auroraClient = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object));
        // Act
        // Assert
    }
9
Joshua Dooms

私の同僚の一人は、ほとんどのHttpClientメソッドがすべて、HttpMessageInvokerの仮想メソッドであるフードの下でSendAsync(HttpRequestMessage request, CancellationToken cancellationToken)を呼び出すことに気付きました。

したがって、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

7
Adam

代替案の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の使用に関するガイダンスはこちら をご覧ください。

6
alastairtree

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

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

HttpClient CreateClient(string name)

したがって、アプリ内で名前付きクライアントを使用する場合、ワイヤーモックに配線された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);
3
Markus Foss

RichardSzalayMockHttpライブラリを使用できます。これは、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'}
2
Justin

私は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
Jorge Aguilar

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

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/

1
j7nn7k

必要なのは、HttpMessageHandler ctorに渡す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
Dogu Arslan

私は多くの答えに納得していません。

まず、HttpClientを使用するメソッドの単体テストを行うことを想像してください。実装でHttpClientを直接インスタンス化しないでください。 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、単体テストをより読みやすくするライブラリ)の助けを借りて、HttpClientを使用するメソッドPostAsyncを単体テストするために必要なものがすべて揃っています

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を使用するものをテストすることでした。これは、そうするためのすてきな方法です。

1
iberodev

これは古い質問ですが、ここでは見られなかった解決策で答えを広げたいと思っています。
Microsoftアセンブリ(System.Net.Http)を偽造して、テスト中にShinsContextを使用できます。

  1. VS 2017では、System.Net.Httpアセンブリを右クリックし、「偽のアセンブリを追加」を選択します
  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
Zeller

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(...);
    }
}
0
thinkOfaNumber