本文需要您了解ASP.NET Core Web API 和 xUnit的相關知識.
這里有xUnit的介紹: https://www.cnblogs.com/cgzl/p/9178672.html#test
ASP.NET Core集成測試官方文檔: https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.1
集成測試 vs 單元測試
測試金字塔, 但它只是一個指導性的概念.
如果所單元測試是對一個組件進行隔離測試的話, 那么集成測試則是測試多個組件共同協作產生出期待的結果.
單元測試通常很快. 而集成測試則慢的多, 因為它需要很多配置, 並且可能依賴於外部的組件, 例如數據庫, 網絡, 文件等.
通常在一個項目里單元測試要比集成測試多很多.
單元測試通常依賴於mock的組件, 而集成測試則使用可運行的組件.
注意: 如果一個行為可以通過單元測試或集成測試來測試的話, 那么應該使用單元測試.
如何進行集成測試
如果我想測試一個API Controller的Action, 我可能需要把這個項目運行起來, 等它跑起來, 發送請求並檢驗結果. 但這樣做的話需要很多的配置工作, 並且很麻煩.
幸好ASP.NET Core 提供了一個Microsoft.AspNetCore.TestHost 庫, 使用它就無需單獨去運行被測試系統了.
ASP.NET Core應用里, 我們在Program.cs里創建WebHostBuilder, 並配置Kestrel Web服務器, 使用Startup類進行應用配置, 注冊服務和中間件等. 最終在WebHostBuilder上使用Build()來創建WebHost的實例, 它可以用來在特定的URL和端口上運行並監聽請求.
而這個TestHost庫也使用了WebHostBuilder, 但它會自己把構建和運行web宿主的工作處理好, 也就是創建出了一個TestServer. TestServer不會在網絡上進行監聽, TestServer創建了一個名為Host的屬性, 它的類型是IWebHost, 它可以用來處理內存里的請求對象. TestServer還會暴露一個HttpClient, 你可以用它來發送請求到被測試系統. 整個交互的過程都是在內存里完成的.
下圖是被測試系統在生產環境和集成測試使用TestServer情形下的對比圖:
圖中:
當應用/被測試系統在生產環境運行的時候, 它使用Kestrel服務器, 監聽HTTP請求, 並把它轉化為HttpContext, 然后再傳進ASP.NET Core的管道里.
TestServer不監聽網絡請求, 它使用HttpClient在內存里發送請求.
仔細看一下集成測試時使用TestServer的流圖:
圖中可以看到: 測試代碼創建TestServer, TestServer創建HttpClient. 測試代碼使用HttpClient發送請求接收響應. TestServer會轉化請求並交給ASP.NET Core MVC/API 應用來處理.
一個例子
首先需要為你的應用建立集成測試項目:
然后需要為項目添加Microsoft.AspNetCore.TestHost 這個庫:
被測試的是這個Controller的GetRoot()所對應的行為, 而不只是這個方法:
測試返回NoContent:
這里面按照之前講的順序, 創建IWebHostBuilder, 並用它創建TestServer, 然后TestServer創建HttpClient. 隨后就使用httpClient發送請求, 返回結果, Assert即可.
需要注意的是, 在創建IWebHostBuilder的時候, 我使用了被測試系統的Startup類來進行配置, 並設定的環境是Development.
由於我這個項目可以看作是真實項目, 所以第一次運行該測試的時候, 測試是Fail的. 因為Startup里面有很多配置並不滿足測試要求.
在我把IpRateLimiting, HttpsRedirection, Authentication, AuthorizeFilter等中間件/組件去掉之后, 測試才通過:
所以這就引出了一個問題, Startup里面的配置在開發時 和 測試時 以及 生產運行時 可能是不太一樣的.
我的Startup里面已經有很多代碼了, 如果再進行環境判斷, 那就會更亂了.
所以我決定為集成測試新建立一個Startup配置類:
ASP.NET Core項目也支持多環境的多個Startup配置類, 這部分內容請參考官方文檔: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-2.1#environment-based-startup-class-and-methods.
然后修改代碼, 使用這個測試專用的Startup即可:
測試會通過.
被測試系統有依賴項
下面繼續測試GetRoot方法的另一個路徑, 這個路徑會用到RootController的依賴項IUrlHelper.
在集成測試里, 通常情況下是不使用Mocking技術的. 所以在這里我也不會mock IUrlHelper:
這里沒有mock任何東西. 此外這個被測試的行為需要設置AcceptHeader.
測試會Pass的, TestServer幫我搞定了一切:
優化測試配置
寫了兩個測試方法, 又引出了一個新的問題: 這兩個方法有一些共同的設置代碼, 這些設置可能會比較耗資源. 我們可以把這些設置放在構造函數里面, 但是如果使用Theory並含有多個InlineData的話, 就會多次運行構造函數里的設置代碼, 可能會非常好資源(時間).
所以我們應該考慮使用test fixture 這里有介紹: http://www.cnblogs.com/cgzl/p/8438019.html#share
而且我們可以使用WebApplicationFactory來構建TestServer, 使用WebApplicationFactory的好處是可以靈活的進行自定義配置.
要使用WebApplicationFactory, 需要添加庫: Microsoft.AspNetCore.Mvc.Testing
使用該庫之后, 代碼應該如下:
但是卻有一個問題, 這里我選擇的時StartupIntegrationTest. 而電腦環境變量設置的是Development, 而調試測試之后發現走的是StartupDevelopment.
也許這是個Bug? 或者就是這樣的意圖. 那我暫時還是使用原始的方法創建TestServer吧, 下面是我使用的代碼:
建立一個TestServerFixture, 需要使用IDisposable來做清理工作:
而測試類注入該Fixture即可:
然后重跑測試, 會pass的:
一個復雜點的例子
我要測試這個Controller下CreateProduct方法對應的行為. 該Controller需要很多依賴項, 其中兩個還需要使用數據庫.
通常情況下集成測試里使用的數據庫和生產環境中使用的數據庫不同, 在測試環境我更傾向於使用內存類數據庫.
EF Core里面至少有兩個內存類的數據庫提供商:
- Microsoft.EntityFrameworkCore.InMemory, 這個都應該知道.
- Microsoft.EntityFrameworkCore.Sqlite. 雖然說Sqlite通常是把數據保存到文件, 但是提供商為它提供了一個內存模式, 把數據庫保存到了內存里.
在StartupIntegrationTest里, 我就使用InMemory吧;
下面是測試方法的代碼:
這代碼其實很簡單, 就是對應着被測試的Controller方法做一些需要的設定即可, 例如Headers, Content-Type等等.
需要注意的是Content-Type是在Content的Header里設置, 而不是Request的Headers里設置, 否則會報亂用Header的錯.
該測試會pass:
最后針對該行為再做一個Model驗證失敗的測試:
沒什么不同, 就是model的Name屬性超長了.
這個測試同樣會通過:
集成測試就簡單介紹這些.......