目錄
許多應用程序僅使用xUnit進行了單元測試,而未進行集成測試。.NET Core提供了進行集成測試的好方法。您的測試將比單元測試更加實際,因爲將僅模擬外部依賴關係而不會模擬內部依賴關係。WireMock.NET提供了執行此操作的方法。
介紹
如果您是執行TDD的ASP.NET Core開發人員,則可能會遇到一些問題。您的Program類和Startup類不在您的測試範圍內。您的模擬框架有助於模擬內部依賴關係,但不會對外部依賴項進行同樣的模擬,例如其他公司創建的web服務。而且,也許您決定不測試某些類,因爲內部依賴太多,無法模擬。在本文中,我將解釋如何解決這些問題。
背景
如果您對.NET Core 3.1的TDD(我在此使用的版本)有一些經驗,最好對xUnit也有一定的經驗,那將會很有幫助。
使用代碼
首先,讓我們實現ConfigureServices方法。我們依賴於appsettings.json文件中設置的外部服務以及依賴於HttpClient的類。
添加了重試策略,以確保在這些請求意外失敗時重試請求。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var googleLocation = Configuration["Google"];
services.AddHttpClient<ISearchEngineService, SearchEngineService>(c =>
c.BaseAddress = new Uri(googleLocation))
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
.AddPolicyHandler(GetRetryPolicy());
}
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError().OrTransientHttpStatusCode()
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
此外,還需要實現用於實例化依賴項注入(到控制器中)的類。只有一種方法。它調用外部服務並返回字符數。
public class SearchEngineService : ISearchEngineService
{
private readonly HttpClient _httpClient;
public SearchEngineService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<int> GetNumberOfCharactersFromSearchQuery(string toSearchFor)
{
var result = await _httpClient.GetAsync($"/search?q={toSearchFor}");
var content = await result.Content.ReadAsStringAsync();
return content.Length;
}
}
從邏輯上講,我們也需要實現控制器。
[Route("api/[controller]")]
[ApiController]
public class SearchEngineController : ControllerBase
{
private readonly ISearchEngineService _searchEngineService;
public SearchEngineController(ISearchEngineService searchEngineService)
{
_searchEngineService = searchEngineService;
}
[HttpGet("{queryEntry}", Name = "GetNumberOfCharacters")]
public async Task<ActionResult<int>> GetNumberOfCharacters(string queryEntry)
{
var numberOfCharacters =
await _searchEngineService.GetNumberOfCharactersFromSearchQuery(queryEntry);
return Ok(numberOfCharacters);
}
}
要使用來自自動化測試的Web請求測試所有內容,我們需要對Web應用程序進行自我託管(在xUnit測試期間)。爲此,我們需要WebApplicationFactory在下面顯示的基類中:
public abstract class TestBase : IDisposable, IClassFixture<WebApplicationFactory<Startup>>
{
protected readonly HttpClient HttpClient;
public TestBase(WebApplicationFactory<Startup> factory, int portNumber, bool useHttps)
{
var extraConfiguration = GetConfiguration();
string afterHttp = useHttps ? "s" : "";
HttpClient = factory.WithWebHostBuilder(whb =>
{
whb.ConfigureAppConfiguration((context, configbuilder) =>
{
configbuilder.AddInMemoryCollection(extraConfiguration);
});
}).CreateClient(new WebApplicationFactoryClientOptions
{
BaseAddress = new Uri($"http{afterHttp}://localhost:{portNumber}")
});
}
protected virtual Dictionary<string, string> GetConfiguration()
{
return new Dictionary<string, string>();
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
HttpClient.Dispose();
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
該基類執行以下操作:
- 創建一個HttpClient對我們自己的應用程序進行REST調用而無需啓動它(由CreateClient完成)
- 運行Startup和Program類中的代碼(也由CreateClient完成)
- 使用AddInMemoryCollection專門針對我們的測試更新配置
- 每次測試後釋放HttpClient
現在我們有了基類,我們可以實現實際的測試了。
public class SearchEngineClientTest : TestBase
{
private FluentMockServer _mockServerSearchEngine;
public SearchEngineClientTest(WebApplicationFactory<Startup> factory) :
base(factory, 5347, false)
{
}
[Theory]
[InlineData("Daan","SomeResponseFromGoogle")]
[InlineData("Sean","SomeOtherResponseFromGoogle")]
public async Task TestWithStableServer(string searchQuery, string externalResponseContent)
{
SetupStableServer(externalResponseContent);
var response = await HttpClient.GetAsync($"/api/searchengine/{searchQuery}");
response.EnsureSuccessStatusCode();
var actualResponseContent = await response.Content.ReadAsStringAsync();
Assert.Equal($"{externalResponseContent.Length}", actualResponseContent);
var requests =
_mockServerSearchEngine.LogEntries.Select(l => l.RequestMessage).ToList();
Assert.Single(requests);
Assert.Contains($"/search?q={searchQuery}", requests.Single().AbsoluteUrl);
}
[Theory]
[InlineData("Daan", "SomeResponseFromGoogle")]
[InlineData("Sean", "SomeOtherResponseFromGoogle")]
public async Task TestWithUnstableServer
(string searchQuery, string externalResponseContent)
{
SetupUnStableServer(externalResponseContent);
var response = await HttpClient.GetAsync($"/api/searchengine/{searchQuery}");
response.EnsureSuccessStatusCode();
var actualResponseContent = await response.Content.ReadAsStringAsync();
Assert.Equal($"{externalResponseContent.Length}", actualResponseContent);
var requests =
_mockServerSearchEngine.LogEntries.Select(l => l.RequestMessage).ToList();
Assert.Equal(2,requests.Count);
Assert.Contains($"/search?q={searchQuery}", requests.Last().AbsoluteUrl);
Assert.Contains($"/search?q={searchQuery}", requests.First().AbsoluteUrl);
}
protected override Dictionary<string, string> GetConfiguration()
{
_mockServerSearchEngine = FluentMockServer.Start();
var googleUrl = _mockServerSearchEngine.Urls.Single();
var configuration = base.GetConfiguration();
configuration.Add("Google", googleUrl);
return configuration;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_mockServerSearchEngine.Stop();
_mockServerSearchEngine.Dispose();
}
}
private void SetupStableServer(string response)
{
_mockServerSearchEngine.Given(Request.Create().UsingGet())
.RespondWith(Response.Create().WithBody(response, encoding:Encoding.UTF8)
.WithStatusCode(HttpStatusCode.OK));
}
private void SetupUnStableServer(string response)
{
_mockServerSearchEngine.Given(Request.Create().UsingGet())
.InScenario("UnstableServer")
.WillSetStateTo("FIRSTCALLDONE")
.RespondWith(Response.Create().WithBody(response, encoding: Encoding.UTF8)
.WithStatusCode(HttpStatusCode.InternalServerError));
_mockServerSearchEngine.Given(Request.Create().UsingGet())
.InScenario("UnstableServer")
.WhenStateIs("FIRSTCALLDONE")
.RespondWith(Response.Create().WithBody(response, encoding: Encoding.UTF8)
.WithStatusCode(HttpStatusCode.OK));
}
}
Web應用程序和外部服務都是自託管的。無需啓動其中之一。我們像進行單元測試一樣進行測試。這是方法的作用:
- SetupStableServer:我們設置了模擬的外部服務,並確保其行爲像穩定的服務。它總是返回狀態碼爲200的響應。
- SetupUnStableServer:這是爲了設置模擬的外部服務,該服務在第一個請求失敗後返回200(500,內部服務器錯誤)
- Dispose:停止外部服務
- GetConfiguration:返回新的配置設置。我們使用模擬的外部服務及其不同的(localhost)URL。
- TestWithStableServer:使用穩定的服務器進行測試。我們調用我們自己的服務,並驗證我們自己的服務發送的請求(必須是一個)是正確的。
- TestWithUnstableServer:一種非常類似的方法,但是由於外部服務表現不穩定,因此預計將發送兩個請求,並且我們有一個重試策略來處理該請求。
興趣點
關於.NET Core的集成測試,有很好的文檔。也有很好的文檔關於WireMock.NET。我剛剛解釋瞭如何結合這些技術,這實際上是一個與衆不同且被低估的主題。集成測試是實現良好代碼覆蓋率,通過REST調用測試應用程序而無需託管和部署的一種非常好的方法,並使測試變得現實,因爲不需要模擬內部依賴項。但是,仍然需要模擬外部依賴關係。否則,測試失敗並不意味着您自己的應用程序太多(外部應用程序可能已關閉),測試成功也並不意味着太多(它無法處理外部服務的意外失敗)。因此,WireMock.NET可以爲您提供幫助。它使您的測試更有意義。
如果您對完整的源代碼感興趣,請訪問GitHub。