とあるリポジトリに貢献しているときに、とても不思議な現象に出会った。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 これは解決したけど、次の頑張ります。