如何使用 WireMock.NET 对 .NET Core 应用程序进行集成测试





5.00/5 (4投票s)
了解如何在模拟 .NET Core 应用程序的外部依赖项时进行集成测试
引言
如果您是一位进行 TDD 的 ASP.NET Core 开发人员,您可能会遇到一些问题。您的 Program
类和 Startup
类未被您的测试覆盖。您的模拟框架可以帮助模拟内部依赖项,但不能对外部依赖项(例如其他公司提供的 Web 服务)执行相同的操作。此外,您可能决定不测试某些类,因为需要模拟的内部依赖项太多。在本文中,我将解释如何解决这些问题。
背景
如果您有一些使用 .NET Core 3.1(我在这里使用的版本)进行 TDD 的经验,最好是使用 xUnit,这将会有很大帮助。
Using the Code
首先,让我们实现 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}://:{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
:这是设置一个模拟的外部服务,该服务在第一次请求失败后(500,Internal Server Error)返回 200Dispose
:停止外部服务GetConfiguration
:返回新的配置设置。我们使用具有不同(localhost)URL 的模拟外部服务。TestWithStableServer
:使用稳定服务器进行测试。我们调用自己的服务并验证我们自己的服务发送的请求(必须只有一个)是否正确。TestWithUnstableServer
:一个非常类似的方法,但是预计会发送两个请求,因为外部服务的行为不稳定,并且我们有一个重试策略来处理这种情况。
关注点
关于使用 .NET Core 进行集成测试,有良好的文档。关于 WireMock.NET,也有良好的文档。我只是解释了如何结合这些技术,这确实是一个不同且被低估的主题。集成测试是实现良好代码覆盖率、通过 REST 调用测试应用程序而无需托管和部署以及使测试真实的好方法,因为不需要模拟内部依赖项。但是,仍然需要模拟外部依赖项。否则,测试的失败并不意味着您的应用程序出现了很多问题(外部应用程序可能已关闭),而成功也不意味着太多(它可能无法处理外部服务的意外故障)。因此,WireMock.NET 可以帮助您。它使您的测试更有意义。
如果您对完整源代码感兴趣,它位于GitHub上。
历史
- 2020 年 5 月 7 日:初始版本