目录
如果您是执行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);
}
}
这个基类做了以下事情:
现在我们有了基类,我们可以实现实际的测试。
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应用程序和外部服务都是自托管的。无需启动其中之一。我们像进行单元测试一样进行测试。这就是方法的作用:
有关于.NET Core集成测试的很好的文档。还有很好的文档关于WireMock.NET。我刚刚解释了如何结合这些技术,这确实是一个不同且被低估的主题。集成测试是实现良好代码覆盖率的一种非常好的方式,通过REST调用测试应用程序而无需托管和部署,并使测试真实,因为不需要模拟内部依赖项。但是,仍然需要模拟外部依赖项。否则,测试失败并不意味着你自己的应用程序(外部应用程序可能宕机),成功也不意味着这么多(它可能无法处理外部服务的意外故障)。因此,WireMock.NET可以帮助您。它使您的测试更有意义。
https://www.codeproject.com/Articles/5267354/How-WireMock-NET-Can-Help-in-Doing-Integration-Tes