大家是如何對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用來做測試用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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請求的測試
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
[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(content); result.Name.Should().Be("name"); } } |
四、Post請求的測試
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
[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, 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等方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
public abstract class ApiTestBase { public abstract string GetBaseAddress(); protected TResult InvokeGetRequest(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(result); } } } } protected TResult InvokePostRequest(string api, TArguemnt arg) { var invoker = CreateMessageInvoker(); using (var cts = new CancellationTokenSource()) { var request = new HttpRequestMessage(HttpMethod.Post, GetBaseAddress() + api); request.Content = new ObjectContent(arg, new JsonMediaTypeFormatter()); using (HttpResponseMessage response = invoker.SendAsync(request, cts.Token).Result) { var result = response.Content.ReadAsStringAsync().Result; return JsonConvert.DeserializeObject(result); } } } private HttpMessageInvoker CreateMessageInvoker() { var config = new HttpConfiguration(); WebApiConfig.Register(config); var server = new HttpServer(config); var messageInvoker = new HttpMessageInvoker(server); return messageInvoker; } } |
有了這個基類,我們寫測試只需要重寫方法GetBaseAddress(),然後直接呼叫基類方法並進行斷言即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
[TestFixture] public class OrderApiTests:ApiTestBase { public override string GetBaseAddress() { return "http://localhost:33203/"; } [Test] public void Should_get_order_successfully() { var result = InvokeGetRequest("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("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啟動起來,然後。。。麻煩
缺點:我覺得原文作者說的那些缺點都可以忽略不計。