原文: Integration Testing
作者: Steve Smith
翻譯: 王健
校對: 孟帥洋(書緣)
集成測試確保應用程序的組件組裝在一起時正常工作。 ASP.NET Core支持使用單元測試框架和可用於處理沒有網絡開銷請求的內置測試的網絡主機集成測試。
章節:
集成測試介紹
集成測試驗證應用程序不同的部位是否正確地組裝在一起。不像單元測試,集成測試經常涉及到應用基礎設施,如數據庫,文件系統,網絡資源或網頁的請求和響應。單元測試用偽造或模擬對象代替這些問題,但集成測試的目的是為了確認該系統與這些系統的預期運行一致。
集成測試,因為它們執行較大的代碼段,並且它們依賴於基礎結構組件,往往要比單元測試慢幾個數量級。因此,限制你寫多少集成測試,特別是如果你可以測試與單元測試相同的行為,是一個不錯的選擇。
提示
如果某些行為可以使用一個單元測試或集成測試進行測試,優先單元測試,因為這幾乎總是會更快的。你可能有幾十或幾百個單元測試有許多不同的輸入,而只是一個集成測試覆蓋了最重要的屈指可數的場景。
在您自己的方法中測試邏輯通常是單元測試的范疇。測試您的應用程序在它的框架內(例如ASP.NET),或是與一個數據庫是否正常運行,是集成測試的工作。它並不需要太多的集成測試,以確認你能寫一行,然后從數據庫中讀取一行。你並不需要測試的數據訪問代碼每一個可能的排列——您僅需要充足的測試來給您信心認為您的應用程序能夠運行良好。
ASP.NET 集成測試
要建立運行集成測試,你需要創建一個測試項目,請參考ASP.NET的Web項目,並安裝測試器。此過程在單元測試中有更詳細的說明,為您命名您的測試和測試類提供了建議。
提示
單獨的單元測試和集成測試使用不同的項目。這有助於確保您不小心將基礎設施問題引入到您的單元測試中,讓您輕松選擇運行所有的測試,或是一組或其他。
測試宿主
ASP.NET包括可添加到集成測試項目的測試宿主和用於托管ASP.NET應用程序,用於處理測試請求,而不需要一個真實的虛擬宿主。所提供的示例包括被配置為使用 xUnit 的集成測試項目和測試主機,您可以從 project.json 文件中進行查看。
"dependencies": {
"PrimeWeb": "1.0.0",
"xunit": "2.1.0",
"dotnet-test-xunit": "1.0.0-rc2-build10025",
"Microsoft.AspNetCore.TestHost": "1.0.0"
},
當Microsoft.AspNet.TestHost包被包含在項目中,您將能夠在您的測試中創建和配置TESTSERVER。下面的測試演示了如何驗證一個對網站的根節點提出了請求並返回的“Hello World!”,並且應該利用Visual Studio中創建的默認ASP.NET空Web模板中成功運行。
private readonly TestServer _server;
private readonly HttpClient _client;
public PrimeWebDefaultRequestShould()
{
// Arrange
_server = new TestServer(new WebHostBuilder()
.UseStartup<Startup>());
_client = _server.CreateClient();
}
[Fact]
public async Task ReturnHelloWorld()
{
// Act
var response = await _client.GetAsync("/");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal("Hello World!",
responseString);
}
這些測試使用安排-執行-斷言的模型,但是在這種情況下,所有的安排步驟都在構造器中完成了,它創建了一個 TestServer 的實例。當您創建 TestServer 時,有好幾種不同的方式來配置它;在這個示例中,我們從被測試的系統(SUT)的 Startup 類中的 Configure 方法進行設置。這種方法可用於配置TestServer請求管道,與如何配置SUT服務器相同。
在測試的行動部分,發起一個對 TestServer 實例的“/”路徑的請求,並且響應讀回字符串。這個字符串將與預期的字符串"Hello World!"進行對比。如果匹配,測試通過,否則測試失敗。
現在我們可以添加一些附加的集成測試,來確認通過web應用程序的素數檢測功能性工作:
public class PrimeWebCheckPrimeShould
{
private readonly TestServer _server;
private readonly HttpClient _client;
public PrimeWebCheckPrimeShould()
{
// Arrange
_server = new TestServer(new WebHostBuilder()
.UseStartup<Startup>());
_client = _server.CreateClient();
}
private async Task<string> GetCheckPrimeResponseString(
string querystring = "")
{
var request = "/checkprime";
if(!string.IsNullOrEmpty(querystring))
{
request += "?" + querystring;
}
var response = await _client.GetAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
[Fact]
public async Task ReturnInstructionsGivenEmptyQueryString()
{
// Act
var responseString = await GetCheckPrimeResponseString();
// Assert
Assert.Equal("Pass in a number to check in the form /checkprime?5",
responseString);
}
[Fact]
public async Task ReturnPrimeGiven5()
{
// Act
var responseString = await GetCheckPrimeResponseString("5");
// Assert
Assert.Equal("5 is prime!",
responseString);
}
[Fact]
public async Task ReturnNotPrimeGiven6()
{
// Act
var responseString = await GetCheckPrimeResponseString("6");
// Assert
Assert.Equal("6 is NOT prime!",
responseString);
}
}
需要注意的是,我們並不是想使用這些測試用例來測試質數檢查程序的正確性,而是確認Web應用程序在我們期待的事情。我們已經有對 PrimeService 充滿信心的單元測試覆蓋率,您可以在這里看到:

注意
您可以從單元測試的文章中了解更多關於單元測試的內容。
現在,我們有一組通過的測試,是一個好的機會來考慮我們是否對設計應用程序的方案感到滿意了。如果我們發現任何 代碼異味,這將是一個重構應用程序來改善設計的好時機。
使用中間件重構
重構是改變一個應用程序的代碼,以提高其設計而不改變其行為的過程。當有一套通過的測試,重構將理想的進行,因為這些有助於確保系統的行為在重構之前和之后保持不變。看看素數檢測邏輯在我們的web應用程序中的實現方式,我們發現:
public void Configure(IApplicationBuilder app,
IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Run(async (context) =>
{
if (context.Request.Path.Value.Contains("checkprime"))
{
int numberToCheck;
try
{
numberToCheck = int.Parse(context.Request.QueryString.Value.Replace("?",""));
var primeService = new PrimeService();
if (primeService.IsPrime(numberToCheck))
{
await context.Response.WriteAsync(numberToCheck + " is prime!");
}
else
{
await context.Response.WriteAsync(numberToCheck + " is NOT prime!");
}
}
catch
{
await context.Response.WriteAsync("Pass in a number to check in the form /checkprime?5");
}
}
else
{
await context.Response.WriteAsync("Hello World!");
}
});
}
這段代碼能正確運行,但遠遠不是我們想在ASP.NET應用中實現這種功能的方式,即使和這段代碼一樣簡單。想象一下,如果我們在每次添加另一個URL終結點時,我們需要在它的代碼中添加那么多代碼,Configure 方法會是什么樣子呢!
一個選擇是,可以考慮在應用程序中添加 MVC ,並創建一個控制器來處理素數檢測。然而,假設我們目前不需要任何其它MVC的功能,這是一個有點矯枉過正。
然而,我們可以利用ASP.NET Core 中間件 的優勢,可以幫助我們在它自己的類中封裝素數檢測的邏輯,並且在 Configure 方法中實現更好的 關注點分離 。
我們想讓中間件使用的路徑被指定為一個參數,所以中間件類在他的構造方法中預留了一個 RequestDelegate 和一個 PrimeCheckerOptions 實例。如果請求的路徑與中間件期望的配置不匹配,我們只需要調用鏈表中的下一個中間件,並不做進一步處理。其余的在 Configure 中的實現代碼,現在在 Invoke 方法中了。
注意
由於我們的中間件取決於PrimeService服務,我們也通過構造函數請求該服務的實例。該框架通過依賴注入來提供這項服務,查看 dependency-injection,假設已經進行了配置(例如在ConfigureServices中)。
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using PrimeWeb.Services;
using System;
using System.Threading.Tasks;
namespace PrimeWeb.Middleware
{
public class PrimeCheckerMiddleware
{
private readonly RequestDelegate _next;
private readonly PrimeCheckerOptions _options;
private readonly PrimeService _primeService;
public PrimeCheckerMiddleware(RequestDelegate next,
PrimeCheckerOptions options,
PrimeService primeService)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (primeService == null)
{
throw new ArgumentNullException(nameof(primeService));
}
_next = next;
_options = options;
_primeService = primeService;
}
public async Task Invoke(HttpContext context)
{
var request = context.Request;
if (!request.Path.HasValue ||
request.Path != _options.Path)
{
await _next.Invoke(context);
}
else
{
int numberToCheck;
if (int.TryParse(request.QueryString.Value.Replace("?", ""), out numberToCheck))
{
if (_primeService.IsPrime(numberToCheck))
{
await context.Response.WriteAsync($"{numberToCheck} is prime!");
}
else
{
await context.Response.WriteAsync($"{numberToCheck} is NOT prime!");
}
}
else
{
await context.Response.WriteAsync($"Pass in a number to check in the form {_options.Path}?5");
}
}
}
}
}
注意
由於這個中間件作為請求委托鏈的一個endpoint,當它的路徑匹配時,在這種情況下這個中間件處理請求時並沒有調用_next.Invoke
有了合適的中間件和一寫有用的擴展方法,使配置更加容易。重構過的 Configure 方法看起來像這樣:
public void Configure(IApplicationBuilder app,
IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UsePrimeChecker();
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
在這重構之后,我們有信心Web應用程序仍然像之前一樣工作,因為我們的集成測試都是通過的。
提示
當您完成重構並且所有測試都通過后,提交您的變更到源代碼管理中,是一個好的主意。如果您正嘗試測試驅動開發,考慮提交代碼到你的 Red-Green-Refacotr 循環中。
總結
集成測試提供了比單元測試更高層次的驗證。它測試應用程序的基礎設施和應用程序的不同部分如何一起工作。 ASP.NET Core 有很大可測試性,並附帶了 TestServer 這使得為Web服務器endpoint連布置集成測試變得非常簡單。
