LoginSignup
5
5

More than 5 years have passed since last update.

HttpClient のモックを理解する

Posted at

とあるリポジトリに貢献しているときに、とても不思議な現象に出会った。HttpClient の SendAsync をモックしているテストコードがあった。本体の方で、EventGrid に HttpRequest を投げている箇所があり、それは単なるステータスをログに吐き出していた。リポジトリオーナーが、ログにEventGrid のリクエストの body も出力してほしいというので、そのようにしたとたんテストが一斉に落ちだした。うーむ。なんでだろ。

ちなみに該当の箇所はこんな感じ

本体

HttpResponseMessage result = await httpClient.PostAsync(this.config.EventGridTopicEndpoint, content);
var body =  await result.Content.ReadAsStringAsync(); 

テスト部

                    var mock = new Mock<HttpMessageHandler>();
                    mock.Protected()
                        .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
                        .Returns((HttpRequestMessage request, CancellationToken cancellationToken) =>
                        {
                            Assert.True(request.Headers.Any(x => x.Key == "aeg-sas-key"));
                            var values = request.Headers.GetValues("aeg-sas-key").ToList();
                            Assert.Single(values);
                            Assert.Equal(eventGridKeyValue, values[0]);
                            Assert.Equal(eventGridEndpoint, request.RequestUri.ToString());
                            var json = request.Content.ReadAsStringAsync().GetAwaiter().GetResult();
                            dynamic content = JsonConvert.DeserializeObject(json);
                            foreach (dynamic o in content)
                            {

最初に思ったのは、ReadAsStringAsync は二回読めないのでは?という推測。では試してみよう。面倒なので、Azure Fucntions でちょこっと。


    // parse query parameter
    string name = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0)
        .Value;
    string name2 ="";
    string data3 ="";
    string data4 = "";
    string data5 = "";
    if (name == null)
    {
        // Get request body
        dynamic data = await req.Content.ReadAsAsync<object>();
        data3 = req.Content.ReadAsStringAsync().GetAwaiter().GetResult();
        data4 = await req.Content.ReadAsStringAsync();
        dynamic data2 = await req.Content.ReadAsAsync<object>();
        var bytes  = await req.Content.ReadAsByteArrayAsync();
        data5 = System.Text.Encoding.UTF8.GetString(bytes);
        name = data?.name;
        name2 = data2?.message;
    }

    return name == null
        ? req.CreateResponse(HttpStatusCode.BadRequest, "Please pass a name on the query string or in the request body")
        : req.CreateResponse(HttpStatusCode.OK, "Hello " + name + "Message " + name2 + "content " + data3 + "content2: " + data4 + "contentbyte:" + data5);
}

実行結果

"Hello ushioMessage content {\"message\": \"hello\", \"name\": \"ushio\"}content2: {\"message\": \"hello\", \"name\": \"ushio\"}contentbyte:{\"message\": \"hello\", \"name\": \"ushio\"}"

なんの問題もなく動いている。インターネットだと、2回はできないから、Stream を使ってカウント戻すべきとか、Byteの方を使うべしとかあったけど、情報が古い、もしくは、Azure Functions の中でLoadIntoBufferAsync 等をつかっているのだろうか、わからない。ここが問題である可能性は少ない。もしくは何らかのロックとかあるのだろうか?

ちいさな Hello World を作ってみる

自分がコードを理解できていなからだと思ったので、ちいなさプログラムを作ってみた。リポジトリのオーナーのコメントもあってわかったのだけど、問題はそこではなく、Mock のクライアントの最後の戻り値で、Contentをセットしていないからと思われる。実際に、Task.FromResult(newHttpResponseMessage(HttpStatusCode.OK);でMockの戻り値を戻すと、null reference exceptionになる。なぜかわからないが、リポジトリの方では null ではなく、本体側の方でデバッグを実行すると、ロックされて次に行かないように見えていた。(もしかすると例外が起こっていた?)いずれにせよ、次の行に行かなかったで、たぶんこれだ。


using Moq;
using Moq.Protected;
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace HttpClientMockSpike
{
    class Program
    {
        public static HttpClient client; 

        public Program()
        {
            var mock = new Mock<HttpMessageHandler>();
            mock.Protected() // add using Moq.Protected;
                  .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) // IExpr is for Protected.
                  .Returns((HttpRequestMessage request, CancellationToken cancellation) =>
                  {
                      var json = request.Content.ReadAsStringAsync().GetAwaiter().GetResult();
                      Console.WriteLine($"Mock: Body: {json}");
                      var content = new StringContent("{\"message\":\"ok!\"}");
                      var message = new HttpResponseMessage(HttpStatusCode.OK);
                      message.Content = content;
                      return Task.FromResult(message);   // Task.FromResult(newHttpResponseMessage(HttpStatusCode.OK); cause null reference exception.
                  });

            client = new HttpClient(mock.Object);
        }
        static void Main(string[] args)
        {
            new Program().MockTesting().GetAwaiter().GetResult();
            Console.ReadLine();
        }

        public async Task MockTesting()
        {
            StringContent content = new StringContent("{\"message\": \"hello\"}", Encoding.UTF8, "application/json");
            HttpResponseMessage result = await client.PostAsync("https://abc.com", content);
            var body = await result.Content.ReadAsStringAsync();
            Console.WriteLine($"HttpClient called: {body}");
        }
    }
}

これで、こちらの方のコードは理解できた。ついでに、Moq のプロテクトメソッドでのテスト方法も理解できた。

まとめ

一見複雑なコードで問題が発生したときは、あれこれ推測してはいけない。それは時間の無駄だ。ちいさなプログラムをつくったり、デバッガをつかってりして、本当に起こっている問題は何かを先に調べるほうが結局速そう。あと、よく知っている人に相談すること。さて、このfix を入れると次の問題が起こったぞw これは解決したけど、次の頑張ります。

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5