目录
介绍
背景
使用代码
兴趣点
许多应用程序仅使用xUnit进行了单元测试,而未进行集成测试。.NET Core提供了进行集成测试的好方法。您的测试将比单元测试更加实际,因为将仅模拟外部依赖关系而不会模拟内部依赖关系。WireMock.NET提供了执行此操作的方法。
- 从GitHub下载完整的源代码
如果您是执行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(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);
}
}
要使用来自自动化测试的Web请求测试所有内容,我们需要对Web应用程序进行自我托管(在xUnit测试期间)。为此,我们需要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可以为您提供帮助。它使您的测试更有意义。
如果您对完整的源代码感兴趣,请访问GitHub。