2016-04-05 18 views
34

単体テストで使用するコードをラップしようとするといくつかの問題があります。問題はこれです。単体テストでHttpClientを嘲笑

public class Connection 
{ 
    private IHttpHandler _httpClient; 

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

:クライアントの実装を注入するsimpleIOCを使用しています

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

そしてConnectionクラス、:

public interface IHttpHandler 
{ 
    HttpClient client { get; } 
} 

そして、それを使用して、クラス、のHttpHandler:私はインターフェイスIHTTPハンドラを持っていますそして、私はこのクラスを持つユニットテストプロジェクトを持っています:

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クラスに用意します。しかし、私はこのクラスの単体テストを書いていますが、明らかに私は実際のバックエンドに対してテストを書くのではなく、模倣したテストを書くことを望んでいます。私は大成功なしにこれに良い答えをGoogleにしようとしました。以前はMoqを使ってモックを使っていたことがありますが、httpClientのようなものでは決してできません。どのように私はこの問題に近づくべきですか?

ありがとうございます。

+1

問題がどこにあるかです。クライアントが 'HttpClient'コンクリートクラスを使用するように強制しています。代わりに、 'HttpClient'の**抽象化**を公開する必要があります。 –

+0

もう少し詳しく説明できますか? Connectionクラスのコンストラクタはどのように構築すればよいですか?HttpClientに他のクラスのConnectionクラスを依存させたくないからです。たとえば、ConnectionのコンストラクタにHttpClientを渡したいのですが、それはHttpClientのConnection依存型を使用する他のすべてのクラスを作成するためですか? – tjugg

+0

興味のある、あなたは何をGoogleにしましたか?明らかにmockhttpはいくつかのSEO改善を使用する可能性があります。 –

答えて

14

あなたのインターフェイスは具体的なHttpClientクラスを公開しています。したがって、このインターフェイスを使用するすべてのクラスはそれに関連付けられています。つまり、これを擬似することはできません。

HttpClientは、どのインターフェイスからも継承されないため、独自に作成する必要があります。あなたは、その後の、可能性があり、このすべてで

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

ポイントはHttpClientHandlerHttpClient、独自に作成することです:私はデコレータのようなパターン勧め:

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

をそして、あなたのクラスには、次のようになります。さまざまな方法でIHttpHandlerを実装する複数のクラスを作成します。

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

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

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

私はまた、あなたが必要とのコメントで述べたhere

+0

これは確かに私の質問に答える。 Nkosisの答えも正しいので、私はどちらを答えとして受け入れるべきかはわかりませんが、私はこれと一緒に行きます。両方の努力をありがとう – tjugg

+0

@tjugg喜んで助けてください。あなたがそれらが有用であると分かったら、答えを投票してください。 – Nkosi

+3

この答えとNkosiの主な違いは、これははるかに薄い抽象であることに注目してください。薄いはおそらく[謙虚なオブジェクト]のために良いです(http://stackoverflow.com/questions/5324049/what-is-the-humble-object-pattern-and-when-is-it-fulful) –

5

Asを参照、これらはまだ非同期メソッドを呼び出すと、非非同期メソッドをテストするが、ユニットテストの非同期メソッドを気にすることの手間をかけずにお勧めします抽象離れて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の実装は、その後包む使用することができます/あなたが本当に望んでいたことは、その機能を提供するサービスだったとしてHTTPリクエストを行うために使用することができる、HttpClientコンクリート/実数またはそのことについては、他のオブジェクトを適応させます具体的には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を使用することに伴う重労働の多くは、抽象化の背後に隠されています。

あなたの接続クラスは、その後のHttpClientの拡張性は、コンストラクタに渡されHttpMessageHandlerにあるあなたのテストは、あなたの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(); 
} 
98

を模擬することができます

public class Connection 
{ 
    private IHttpClient _httpClient; 

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

抽象化されたクライアントを注入することができます。その意図は、プラットフォーム固有の実装を可能にすることですが、あなたもそれを模擬することができます。 HttpClientのデコレータラッパーを作成する必要はありません。

あなたが部品番号を使用して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'} 
+0

私はちょうどメッセージハンドラのHttphandlerクラスとしてMockHttpMessageHandlerを渡しますか?または、自分のプロジェクトでどのように実装したのですか? – tjugg

+0

はい、MockHttpMessageHandlerをHttpClientコンストラクタに渡してから、そのHttpClientをテストする前にコンポーネントに渡します。 –

+0

しかし、コンポーネントはHttpMessageHandler自体に依存し、HttpClientを作成することもできます。あるいは、注入ができない場合は、HttpClientFactoryを使ってモックを登録してみることもできます(私は決して最後のメソッドを使ったことはありません)。 –

12

をこれが一般的な質問である、と私はひどく上でしたあなたはHttpClientを模倣する能力を望んでいる側ですが、私はHttpClientを嘲笑してはいけないことを最終的に実感しました。それは論理的だと思われますが、私たちはオープンソースライブラリに見られるようなものによって洗脳されてきたと思います。

「クライアント」がありますので、私たちがコードを嘲笑して孤立してテストすることができるので、自動的に同じ原則をHttpClientに適用しようとします。 HttpClientは実際には多くのことを行います。あなたはそれをHttpMessageHandlerのマネージャーと考えることができるので、それを嘲笑したくないので、のまだにはインターフェイスがありません。あなたが実際にユニットテストやサービスの設計に関心を持っているのはHttpMessageHandlerなので、レスポンスを返すのですから、です。

また、HttpClientをもっと大きな処理のように扱うべきであることを指摘しておく価値があります。たとえば、新しいHttpClientの導入を最小限に抑えます。それらを再利用するように設計されています。より大きな取引のように扱い始めると、それを模倣したいと思うはるかに間違った気持ちになり、メッセージハンドラは、クライアントではなく、あなたが注入しているものになり始めるでしょう。

つまり、クライアントではなくハンドラの周囲に依存関係を設計します。 HttpClientを使用する抽象的な "サービス"であっても、ハンドラを挿入して、その代わりにあなたの注射可能な依存関係として使用することができます。あなたのテストでは、ハンドラを偽装してテストを設定するためのレスポンスを制御することができます。

ラッピングHttpClientは非常識な時間の無駄です。私は、問題はあなたがほんの少し上下逆さまにそれを持っているということだと思います

[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

ビルは、私は、任意の外部の依存関係を持っていない、このコードを、示唆しています。

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 
    } 
+1

モックを効果的にテストしています。モックの本当のパワーは、期待を設定し、各テストでその動作を変更できることです。 'HttpMessageHandler'を実装しなければならないという事実は、あなた自身が不可能にすることを可能にします - そして、メソッドは' protected internal'であるからです。 – MarioDS

4

:他の回答に

8

私は最善のアプローチはHttpMessageHandlerをモックではなく、HttpClientををラップしていることを他の回答の一部に同意します。この回答は、依然としてHttpClientを注入し、それがシングルトンであること、または依存性注入を伴って管理されるという点で独特です。

"HttpClientは、一度インスタンス化され、アプリケーションのライフサイクルを通じて再利用されることを意図しています。" (https://docs.microsoft.com/en-us/aspnet/web-api/overview/advanced/calling-a-web-api-from-a-net-client)。

HttpMessageHandlerは、SendAsyncが保護されているため、少し厄介なことがあります。 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>()) 
       .Returns(Task.FromResult(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()'はキラーでした。この例をありがとう。ソースをまったく変更せずにテストを書くことができます。 – tyrion

2

1つの選択肢は、あなたが本当のHTTPリクエストではないモックをテストする意味、セットアップに要求URLパターンマッチングに基づいて缶詰のレスポンスを返すスタブ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'}") 
    ); 

あなたはより多くの細部を見つけるとguidance on using wiremock in tests here.

0

することができますあなたはHttpMessageHandlerをあざけりやテスト中に使用されるのHttpClientオブジェクトを返すことができRichardSzalay MockHttpライブラリを使用することができます。

GitHub MockHttp

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

From the GitHub documentation

MockHttpは、流暢なコンフィギュレーションのAPIを提供し、缶詰を提供HttpClientをを駆動するエンジンを、交換HttpMessageHandlerを定義します応答。呼び出し元(たとえば、アプリケーションのサービス層)は、その存在を認識しません。お使いのインタフェースで `HttpClient`を公開

Example from 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'} 
関連する問題