在开发.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);
}
}
这个基类做了以下几件事情:
现在已经有了基类,可以实施实际的测试。
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应用程序和外部服务都是自托管的。不需要启动其中任何一个。像进行单元测试一样进行测试。以下是这些方法的作用:
有关.NET Core集成测试的,以及有关WireMock.NET的。只是解释了如何结合这些技术,这是一个真正不同且被低估的主题。集成测试是实现良好代码覆盖率的好方法,通过REST调用测试应用程序而无需托管和部署,并使测试更真实,因为不需要模拟内部依赖。然而,外部依赖仍然需要模拟。否则,测试的失败并不意味着您的应用程序有问题(外部应用程序可能已经关闭),成功也不意味着很多(它可能无法处理外部服务的意外失败)。因此,WireMock.NET在这里提供帮助。它使您的测试更有意义。