在单元测试中多次模拟httpclient

4

你好,我正在尝试模拟httpclient,以便强制它给我需要的结果。 问题在于 ,在我的单元测试中,我必须两次模拟httpClient,每次响应都不同。

...
...
            var httpReq = new HttpRequestMessage(
            HttpMethod.Get,
            $"{config["MY_TEST_ENDPOINT"]/site1}");

            var myContent = await response.Content.ReadAsStringAsync();
            var myFirstModel = JsonConvert.DeserializeObject<MyFirstModel>(myContent );
...
...

我嘲笑这个可能性:

...
myMockedHttp
            .Protected()
            .Setup<Task<HttpResponseMessage>>(
                "SendAsync",
                ItExpr.IsAny<HttpRequestMessage>(),
                ItExpr.IsAny<CancellationToken>())
            .ReturnsAsync(new HttpResponseMessage()
            {
                StatusCode = HttpStatusCode.OK,
                Content = new StringContent("[{'id':'0001'," +
                                            "'name':'Jonah'," +
                                            "'street':'Cambridge Street 1234'}]"
                                            )
            })
            .Verifiable();

这将给我一个响应,我可以进行反序列化并使用它,提供一个具有3个成员id、名称和街道的对象。

对我来说,问题在于同一单元测试中还有另一个httpclient调用,但我不知道如何模拟它。它是这样的:

...
...
            var httpReq = new HttpRequestMessage(
            HttpMethod.Get,
            $"{config["MY_TEST_ENDPOINT"]/site2}");

            var httpResponseMsg = await _httpClient.SendAsync(httpReq);

            var myContent = await response.Content.ReadAsStringAsync();
            var myFirstModel = JsonConvert.DeserializeObject<MySecondModel>(myContent );
...
...

如果它是唯一的httpclient调用,最初我会这样模拟它:

...
myMockedHttp
            .Protected()
            .Setup<Task<HttpResponseMessage>>(
                "SendAsync",
                ItExpr.IsAny<HttpRequestMessage>(),
                ItExpr.IsAny<CancellationToken>())
            .ReturnsAsync(new HttpResponseMessage()
            {
                StatusCode = HttpStatusCode.OK,
                Content = new StringContent("[{'id':'34'," +
                                            "'insurance':'Etx'," +
                                            "'insider':'daily'," +
                                            "'collectives':'4'}]"
                                            )
            })
            .Verifiable();
...
...

但是很明显,我不能...问题是,我从未尝试过这种情况,即需要模拟多个httpclient调用。正如您所看到的,它们响应不同的内容,因为它们是不同的对象,需要以这种方式处理。

有人有什么办法解决这个问题吗?


2
不要使用 ItExpr.IsAny<HttpRequestMessage>(),它允许任何请求。相反,告诉模拟对象你想要模拟的确切请求以及该请求应该得到的响应。 - Heretic Monkey
@HereticMonkey,你能否提供一下那行代码的例子? - nelion
1
我已经有一段时间没有使用Moq了,但我记得做过类似于It.Is<HttpRequestMessage>((message) => message.RequestUri.Host.Contains("somesite1"))的事情。 - Heretic Monkey
它可以工作!但是要使用ItExpr.Is而不是It.Is,因为必须使用.Protected。 - nelion
使用 Moq 来模拟 HttpClient 结果会使测试代码非常混乱。只需创建自己的假响应处理程序类,该类继承自 HttpMessageHandler。 - bytedev
1个回答

6

有时候,设置一个模拟对象并不总是最好的方法。由于HttpClient非常依赖于HttpMessageHandler,因此您可以创建一个简单的HttpMessageHandler来处理预期请求。

class TestMessageHandler : HttpMessageHandler {
    private readonly IDictionary<string, HttpResponseMessage> messages;

    public TestMessageHandler(IDictionary<string, HttpResponseMessage> messages) {
        this.messages = messages;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
        var response = new HttpResponseMessage(HttpStatusCode.NotFound);
        if (messages.ContainsKey(request.RequestUri.ToString()))
            response = messages[request.RequestUri.ToString()] ?? new HttpResponseMessage(HttpStatusCode.NoContent);
        response.RequestMessage = request;
        return Task.FromResult(response);
    }
}

上述处理程序仅使用传入请求的URL在字典中查找响应。如果需要,可以进行重构以处理更复杂的请求,但对于简单的请求,这将足够。

将处理程序传递给为测试创建的客户端

public async Task MyTestMethod() {
    //Arrange
    Dictionary<string, HttpResponseMessage> messages = new Dictionary<string, HttpResponseMessage>();
    messages.Add("https://somesite1.com/ping", new HttpResponseMessage() {
        StatusCode = HttpStatusCode.OK,
        Content = new StringContent("[{'id':'0001'," +
                                    "'name':'Jonah'," +
                                    "'street':'Cambridge Street 1234'}]"
                                    )
    });
    messages.Add("https://somesite2.com/ping", new HttpResponseMessage() {
        StatusCode = HttpStatusCode.OK,
        Content = new StringContent("[{'id':'34'," +
                                    "'insurance':'Etx'," +
                                    "'insider':'daily'," +
                                    "'collectives':'4'}]"
                                    )
    });

    var client = new HttpClient(new TestMessageHandler(messages));

    //...inject client as needed

具有自定义处理程序的客户端现在可以根据需要调用,以验证测试主体的预期行为。

哦,我在“var response = messages[request.RequestUri.ToString()] ?? new HttpResponseMessage(HttpStatusCode.NotFound);”处收到了“字典中未找到给定的键 'xxxx'”的错误信息。既然这是一个模拟,它不应该考虑那些值吗? - nelion
谢谢您的快速回答,但它只返回404而不是200?我把“var response = new HttpResponseMessage(HttpStatusCode.NotFound);”改成“var response = new HttpResponseMessage(HttpStatusCode.OK);”有做错什么吗?这样就会返回状态码200。 - nelion
嗯...这很奇怪...如果我不写HttpStatusCode.OK而是写.NotFound,我会得到一个404错误。当我把它改成.OK时,“myContent”(或response.Content)在“var myContent = await response.Content.ReadAsStringAsync();”中为null,并且这是你建议的代码副本,我已经使用了。 - nelion
1
@badaboomskey请确保在客户端调用的URL与添加到字典中的URL相同。 - Nkosi
啊!!是的,它们必须完全相同。太好了! - nelion
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接