大家是如何對webApi寫測試的呢?
1.利用Fiddler直接做請求,觀察response的內容。
2.利用Httpclient做請求,斷言response的內容。
3.直接調用webApi的action,這種方式的測試跟真實的調用還是有一定差距,不夠完美。
接下來我介紹一種webApi的in-memory調用方法,也能夠達到對webApi的測試,並且由於是in-memory調用,效率也比較高,非常適寫單元測試。本文參考了In memory client, host and integration testing of your Web API service。
一、首先寫一個OrderController用來做測試用
public class OrderController : ApiController
{
// GET api/order
public Order Get()
{
return new Order(){Id = 1,Descriptions = "descriptions",Name = "name"};
}
// GET api/order/5
public string Get(int id)
{
return "value";
}
// POST api/order
public Order Post(Order order)
{
return order;
}
// DELETE api/order/5
public void Delete(int id)
{
}
}
二、WebApi的請求過程
webApi的核心是對消息的管道處理,整個核心是有一系列消息處理器(HttpMessageHandler)首尾連接的雙向管道,管道頭為HttpServer,管道尾為HttpControllerDispatcher,HttpControllerDispatcher負責對controller的激活和action的執行,然后相應的消息逆向流出管道。
所以我們可以利用HttpMessageInvoker將一個請求消息HttpRequestMessage發送到管道中,最后收到的消息HttpResponseMessage就代表一個真實的請求響應。
三、Get請求的測試
[Test]
public void GetTest()
{
string baseAddress = "http://localhost:33203/";
HttpConfiguration config = new HttpConfiguration();
WebApiConfig.Register(config);
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
HttpServer server = new HttpServer(config);
HttpMessageInvoker messageInvoker = new HttpMessageInvoker(server);
CancellationTokenSource cts = new CancellationTokenSource();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, baseAddress + "api/order");
using (HttpResponseMessage response = messageInvoker.SendAsync(request, cts.Token).Result)
{
var content = response.Content.ReadAsStringAsync().Result;
var result = JsonConvert.DeserializeObject<Order>(content);
result.Name.Should().Be("name");
}
}
四、Post請求的測試
[Test]
public void PostTest()
{
string baseAddress = "http://localhost:33203/";
HttpConfiguration config = new HttpConfiguration();
WebApiConfig.Register(config);
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
HttpServer server = new HttpServer(config);
HttpMessageInvoker messageInvoker = new HttpMessageInvoker(server);
CancellationTokenSource cts = new CancellationTokenSource();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, baseAddress + "api/order");
var order = new Order() { Id = 1, Name = "orderName", Descriptions = "orderDescriptions" };
request.Content = new ObjectContent<Order>(order, new JsonMediaTypeFormatter());
using (HttpResponseMessage response = messageInvoker.SendAsync(request, cts.Token).Result)
{
var content = JsonConvert.SerializeObject(order, new JsonSerializerSettings() { ContractResolver = new CamelCasePropertyNamesContractResolver() });
response.Content.ReadAsStringAsync().Result.Should().Be(content);
}
}
四、重構
可以看到這兩個測試大部分的代碼是相同的,都是用來發送請求。因此我們提取一個webApiTestBase類,該基類可以提供InvokeGetRequest,InvokePostRequest,InvokePutRequest等方法
public abstract class ApiTestBase
{
public abstract string GetBaseAddress();
protected TResult InvokeGetRequest<TResult>(string api)
{
using (var invoker = CreateMessageInvoker())
{
using (var cts = new CancellationTokenSource())
{
var request = new HttpRequestMessage(HttpMethod.Get, GetBaseAddress() + api);
using (HttpResponseMessage response = invoker.SendAsync(request, cts.Token).Result)
{
var result = response.Content.ReadAsStringAsync().Result;
return JsonConvert.DeserializeObject<TResult>(result);
}
}
}
}
protected TResult InvokePostRequest<TResult, TArguemnt>(string api, TArguemnt arg)
{
var invoker = CreateMessageInvoker();
using (var cts = new CancellationTokenSource())
{
var request = new HttpRequestMessage(HttpMethod.Post, GetBaseAddress() + api);
request.Content = new ObjectContent<TArguemnt>(arg, new JsonMediaTypeFormatter());
using (HttpResponseMessage response = invoker.SendAsync(request, cts.Token).Result)
{
var result = response.Content.ReadAsStringAsync().Result;
return JsonConvert.DeserializeObject<TResult>(result);
}
}
}
private HttpMessageInvoker CreateMessageInvoker()
{
var config = new HttpConfiguration();
WebApiConfig.Register(config);
var server = new HttpServer(config);
var messageInvoker = new HttpMessageInvoker(server);
return messageInvoker;
}
}
有了這個基類,我們寫測試只需要重寫方法GetBaseAddress(),然后直接調用基類方法並進行斷言即可
[TestFixture]
public class OrderApiTests:ApiTestBase
{
public override string GetBaseAddress()
{
return "http://localhost:33203/";
}
[Test]
public void Should_get_order_successfully()
{
var result = InvokeGetRequest<Order>("api/order");
result.Name.Should().Be("name");
result.Descriptions.Should().Be("descriptions");
result.Id.Should().Be(1);
}
[Test]
public void Should_post_order_successfully()
{
var newOrder=new Order(){Name = "newOrder",Id = 100,Descriptions = "new-order-description"};
var result = InvokePostRequest<Order,Order>("api/order", newOrder);
result.Name.Should().Be("newOrder");
result.Id.Should().Be(100);
result.Descriptions.Should().Be("new-order-description");
}
}
是不是干凈多了。
這種in-memory的測試方案有什么優點和缺點呢?
優點:
1.模擬真實調用,需要傳入api地址即可得到結果,由於整個調用是in-memory的,所有效率很高,很適合集成測試。
2.整個測試時可以調試的,可以直接從單元測試調試進去,如果你寫一個httpClient的測試,需要把webApi啟動起來,然后。。。麻煩
缺點:我覺得原文作者說的那些缺點都可以忽略不計。
