本文需要您瞭解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屬性超長了.
這個測試同樣會通過:
整合測試就簡單介紹這些.......