我正在尝试对调用 API 的函数进行单元测试。我已经使用模拟
HttpMessageHandler
成功完成了此操作,如下所示,它允许我伪造来自 API 的响应:
private static Mock<HttpMessageHandler> GetMockHttpMessageHandler(string mockResponse)
{
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(mockResponse)
}));
return mockMessageHandler;
}
到目前为止,一切都很好。我已经能够测试一半的函数,后半部分进行另一个 api 调用 - 然后两个响应都被包装到系统使用的对象中。问题是,第二个 api 需要有不同的模拟响应。
我以为我可以将上面代码中的
ItExpr.IsAny<HttpRequestMessage>()
更改为 new HttpRequestMessage(HttpMethod.Post, "http://LiveUrl.com/AuthenticateUserCredential")
,然后有多个 Setup/Returns
,响应根据 URI 进行更改,但我尝试如下(只有一个 Setup/Return
为了测试我没有打破前半部分测试)
private static Mock<HttpMessageHandler> GetMockHttpMessageHandler(string mockResponse)
{
var mockMessageHandler = new Mock<HttpMessageHandler>();
mockMessageHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", new HttpRequestMessage(HttpMethod.Post, "http://LiveUrl.com/AuthenticateUserCredential"), ItExpr.IsAny<CancellationToken>())
.Returns(Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(mockResponse)
}));
return mockMessageHandler;
}
现在上面的内容破坏了第一个 api 调用 - 我得到以下响应:
处理程序未返回响应消息
现在我陷入了困境 - 我想做的事情可能吗?
认为在Moq中使用SetupSequence可以解决这个问题:
private HttpClient GetHttpClientWithHttpMessageHandlerSequenceResponseMock(List<Tuple<HttpStatusCode,HttpContent>> returns)
{
var handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var handlerPart = handlerMock
.Protected()
// Setup the PROTECTED method to mock
.SetupSequence<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
);
foreach (var item in returns)
{
handlerPart = AddReturnPart(handlerPart,item.Item1,item.Item2);
}
handlerMock.Verify();
// use real http client with mocked handler here
var httpClient = new HttpClient(handlerMock.Object)
{
BaseAddress = new Uri("http://test.com/"),
};
return httpClient;
}
private Moq.Language.ISetupSequentialResult<Task<HttpResponseMessage>> AdddReturnPart(Moq.Language.ISetupSequentialResult<Task<HttpResponseMessage>> handlerPart,
HttpStatusCode statusCode, HttpContent content)
{
return handlerPart
// prepare the expected response of the mocked http call
.ReturnsAsync(new HttpResponseMessage()
{
StatusCode = statusCode, // HttpStatusCode.Unauthorized,
Content = content //new StringContent("[{'id':1,'value':'1'}]"),
});
}
调用上面的代码:
public void ExceuteMultipleHttpCalls()
{
var contentSequence1 = new StringContent("{ 'id':'anId','email':'[email protected]'}", Encoding.UTF8, "application/json");
var contentSequence2 = new StringContent("{ 'id':'anotherId','email':'[email protected]'}", Encoding.UTF8, "application/json");
var sequenceResponse = new List<Tuple<HttpStatusCode, HttpContent>>
{
new Tuple<HttpStatusCode, HttpContent>(HttpStatusCode.OK, contentSequence1),
new Tuple<HttpStatusCode, HttpContent>(HttpStatusCode.Created, contentSequence2)
};
HttpClient httpClient = GetHttpClientWithHttpMessageHandlerSequenceResponseMock(sequenceResponse);
//use this httpClient to call function where this client is called multiple times.
}
调用 API 的函数无法进行单元测试,这不是单元测试,而是集成测试。 如果你想简化你的生活,请开始以实用的方式思考。
你有一个调用 API 的函数,假设它看起来像这样:
public string DoSomething()
{
string myData = //here we call another API
//here we do something with the response from the API
}
现在,在函数之外进行 API 调用:
string myData = // api call
DoSomething(myData);
public string DoSomething(string myData)
{
// here we do something with myData
//this function no longer cares how it got the data.
}
现在您可以对您的功能进行单元测试,并且可以检查实际功能和业务规则,而不必担心模拟任何内容。一切都变得简单多了。
您仍然可以通过集成测试来测试实际的 API,这些测试将检查您是否返回正确的 HTTP 代码、异常、模型验证是否正确等。
你可以这样做:
var requestUri = "http://LiveUrl.com/AuthenticateUserCredential";
var response = Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(mockResponse)
})
mockMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.Is<HttpRequestMessage>(x => x.RequestUri.AbsoluteUri.StartsWith(requestUri)),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(response);
对于那些感兴趣的人,模拟多个 HTTP 请求的 NSubstitute 版本将如下所示:
var requestUri = "http://LiveUrl.com/AuthenticateUserCredential";
var response = Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(mockResponse)
});
var mockClient = Substitute.For<HttpClient>();
mockClient.SendAsync(
Arg.Is<HttpRequestMessage>(x => x.RequestUri.AbsoluteUri.StartsWith(requestUri)),
Arg.Any<CancellationToken>())
.Returns(response);
不确定以前的版本是否不允许这样做,但是如果您在同一方法中有不同的 HTTP 调用,您可以通过指定 HttpRequestMessage 来对每个调用进行 Moq,与您所做的类似。与您的代码的唯一区别是不是将其初始化为新的
new HttpRequestMessage()
将其添加为
ItExpr.Is<HttpRequestMessage>(PREDICATE BY WHICH YOU WILL MATCH YOUR ROUTE)
例如:
private static Mock<HttpMessageHandler> GetMockHttpMessageHandler(string mockResponse)
{
var mockMessageHandler = new Mock<HttpMessageHandler>();
mockMessageHandler.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
//BELOW IS PREDICATE
ItExpr.Is<HttpRequestMessage>(match =>
match.Method == HttpMethod.Post && match.RequestUri == "http://LiveUrl.com/AuthenticateUserCredential"),
//END OF PREDICATE
ItExpr.IsAny<CancellationToken>())
.Returns(Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(mockResponse)
}));
return mockMessageHandler;
}
然后您可以在该设置下方添加另一个设置以使用不同的 URL 或使用不同的 HTTP 方法
var mockHttp = new MockHttpMessageHandler();
mockHttp
.When("http://localhost/api/user/*")
.Respond("application/json", "{'name' : 'Test McGee'}");
var client = mockHttp.ToHttpClient(); // pass this to your SUT
如果您对 HttpClient 进行了大量测试,无论如何您最终都会以 helper utils 的形式“重新发明”它