.NET Core集成测试实践

在开发.NET Core应用程序时,可能会遇到一些测试难题,比如无法覆盖Program类和Startup类,或者因为内部依赖过多而无法模拟某些类。此外,可能还会遇到外部依赖,如其他公司提供的网络服务,这些依赖无法通过现有的模拟框架进行模拟。本文将介绍如何解决这些问题。

代码实现

首先,需要实现ConfigureServices方法。这个方法依赖于appsettings.json文件中设置的外部服务,以及依赖于HttpClient的类。添加了一个重试策略,以确保在请求意外失败时能够重试。

public void ConfigureServices(IServiceCollection services) { services.AddControllers(); var googleLocation = Configuration["Google"]; services.AddHttpClient(c => c.BaseAddress = new Uri(googleLocation)) .SetHandlerLifetime(TimeSpan.FromMinutes(5)) .AddPolicyHandler(GetRetryPolicy()); } private static IAsyncPolicy 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 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> GetNumberOfCharacters(string queryEntry) { var numberOfCharacters = await _searchEngineService.GetNumberOfCharactersFromSearchQuery(queryEntry); return Ok(numberOfCharacters); } } >

为了通过自动化测试进行网络请求测试,需要在xUnit测试期间自托管Web应用程序。为此,需要使用WebApplicationFactory。

public abstract class TestBase : IDisposable, IClassFixture> { protected readonly HttpClient HttpClient; public TestBase(WebApplicationFactory 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 GetConfiguration() { return new Dictionary(); } 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 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 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在这里提供帮助。它使您的测试更有意义。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485