AspNetCore 集成测试多个 WebApplicationFactory 实例?
AspNetCore Integration Testing Multiple WebApplicationFactory Instances?
有人知道是否可以在同一个单元测试中托管 WebApplicationFactory<TStartop>()
的多个实例吗?
我已经尝试过了,但似乎无法解决这个问题。
即
_client = WebHost<Startup>.GetFactory().CreateClient();
var baseUri = PathString.FromUriComponent(_client.BaseAddress);
_url = baseUri.Value;
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", "Y2E890F4-E9AE-468D-8294-6164C59B099Y");
WebHost
只是一个帮手 class 让我可以轻松地在一行中建立工厂然后建立客户。
在幕后它所做的是这样的:
new WebApplicationFactory<TStartup>()
还有其他一些事情。
会很好如果我能建立另一个不同的 web 服务器的实例来测试服务器到服务器的功能。
有谁知道这是否可能?
没有。这是不可能的。 WebApplicationFactory
依靠 xUnit 的 IClassFixture
,它必须在 class 级别应用,这意味着您只能咬一口苹果。 WebApplicationFactory
本身可以根据测试进行自定义,这可以满足大多数需要 "different" 的用例,但它无助于同时需要两个完全独立的活动测试服务器时间.
但是,也就是说,您首先想要的是糟糕的测试设计。测试的全部意义在于消除变量,这样您就可以真正确保 SUT 的一部分确实在工作。即使在集成测试环境中,您仍然只是在查看应用程序各部分之间的一种特定交互。有两台测试服务器,相互馈送,有效地增加变量,让您无法保证任何一方都在正常工作。
与公认的答案相反,使用两个 WebApplicationFactory
实例测试服务器到服务器的功能实际上非常容易:
public class OrderAPIFactory : WebApplicationFactory<Order>
{
public OrderAPIFactory() { ... }
protected override void ConfigureWebHost(IWebHostBuilder builder) { ... }
}
public class BasketAPIFactory : WebApplicationFactory<BasketStartup>
{
public BasketAPIFactory() { ... }
protected override void ConfigureWebHost(IWebHostBuilder builder) { ... }
}
然后您可以按如下方式实例化自定义工厂:
[Fact]
public async Task TestName()
{
var orderFactory = new OrderAPIFactory();
var basketFactory = new BasketAPIFactory();
var orderHttpClient = orderFactory.CreateClient();
var basketHttpClient = basketFactory.CreateClient();
// you can hit eg an endpoint on either side that triggers server-to-server communication
var orderResponse = await orderHttpClient.GetAsync("api/orders");
var basketResponse = await basketHttpClient.GetAsync("api/basket");
}
我也不同意关于它必然是糟糕设计的公认答案:它有它的用例。我的公司有一个微服务基础设施,它依赖 data duplication across microservices and uses an async messaging queue with integration events 来确保数据的一致性。不用说,消息传递功能起着核心作用,需要进行适当的测试。此处描述的测试设置在这种情况下非常有用。例如,它允许我们彻底测试在发布消息时已关闭的服务如何处理消息:
[Fact]
public async Task DataConsistencyEvents_DependentServiceIsDown_SynchronisesDataWhenUp()
{
var orderFactory = new OrderAPIFactory();
var orderHttpClient = orderFactory.CreateClient();
// a new order is created which leads to a data consistency event being published,
// which is to be consumed by the BasketAPI service
var order = new Order { ... };
await orderHttpClient.PostAsync("api/orders", order);
// we only instantiate the BasketAPI service after the creation of the order
// to mimic downtime. If all goes well, it will still receive the
// message that was delivered to its queue and data consistency is preserved
var basketFactory = new BasketAPIFactory();
var basketHttpClient = orderFactory.CreateClient();
// get the basket with all ordered items included from BasketAPI
var basketResponse = await basketHttpClient.GetAsync("api/baskets?include=orders");
// check if the new order is contained in the payload of BasketAPI
AssertContainsNewOrder(basketResponse, order);
}
可以在单个集成测试中承载 WebApplicationFactory 的多个通信实例。
假设我们有一个名为 WebApplication
的主服务,它依赖于名为 WebService
的实用程序服务,使用名为“WebService”的 HttpClient。
这是集成测试的例子:
[Fact]
public async Task GetWeatherForecast_ShouldReturnSuccessResult()
{
// Create application factories for master and utility services and corresponding HTTP clients
var webApplicationFactory = new CustomWebApplicationFactory();
var webApplicationClient = webApplicationFactory.CreateClient();
var webServiceFactory = new WebApplicationFactory<Startup>();
var webServiceClient = webServiceFactory.CreateClient();
// Mock dependency on utility service by replacing named HTTP client
webApplicationFactory.AddHttpClient(clientName: "WebService", webServiceClient);
// Perform test request
var response = await webApplicationClient.GetAsync("weatherForecast");
// Assert the result
response.EnsureSuccessStatusCode();
var forecast = await response.Content.ReadAsAsync<IEnumerable<WeatherForecast>>();
Assert.Equal(10, forecast.Count());
}
此代码需要 CustomWebApplicationFactory
class 才能实现:
// Extends WebApplicationFactory allowing to replace named HTTP clients
internal sealed class CustomWebApplicationFactory
: WebApplicationFactory<WebApplication.Startup>
{
// Contains replaced named HTTP clients
private ConcurrentDictionary<string, HttpClient> HttpClients { get; } =
new ConcurrentDictionary<string, HttpClient>();
// Add replaced named HTTP client
public void AddHttpClient(string clientName, HttpClient client)
{
if (!HttpClients.TryAdd(clientName, client))
{
throw new InvalidOperationException(
$"HttpClient with name {clientName} is already added");
}
}
// Replaces implementation of standard IHttpClientFactory interface with
// custom one providing replaced HTTP clients from HttpClients dictionary
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.ConfigureServices(services =>
services.AddSingleton<IHttpClientFactory>(
new CustomHttpClientFactory(HttpClients)));
}
}
最后,CustomHttpClientFactory
class 是必需的:
// Implements IHttpClientFactory by providing named HTTP clients
// directly from specified dictionary
internal class CustomHttpClientFactory : IHttpClientFactory
{
// Takes dictionary storing named HTTP clients in constructor
public CustomHttpClientFactory(
IReadOnlyDictionary<string, HttpClient> httpClients)
{
HttpClients = httpClients;
}
private IReadOnlyDictionary<string, HttpClient> HttpClients { get; }
// Provides named HTTP client from dictionary
public HttpClient CreateClient(string name) =>
HttpClients.GetValueOrDefault(name)
?? throw new InvalidOperationException(
$"HTTP client is not found for client with name {name}");
}
您可以在此处找到示例的完整代码:https://github.com/GennadyGS/AspNetCoreIntegrationTesting
这种方法的优点是:
- 能够测试服务之间的交互;
- 无需模拟服务的内部结构,以便您可以将它们视为黑盒;
- 测试对于任何重构(包括通信协议的更改)都是稳定的;
- 测试快速、独立、不需要任何先决条件并给出可预测的结果。
这种方法的主要 缺点 是参与服务(例如不同主要版本的 EFCore)在现实世界场景中可能存在冲突的依赖关系,因为所有服务都在测试中使用在单个进程中 运行。
有几种缓解此类问题的方法。其中之一是将模块化方法应用于服务的实现,并根据配置文件在运行时加载模块。这可能允许在测试中替换配置文件,从加载中排除几个模块,并用更简单的模拟替换缺失的服务。您可以在上面示例存储库的分支“模块化”中找到应用这种方法的示例。
我基于 Gennadii Saltyshchak 的解决方案创建了这个,这正是我一直在寻找的:两台服务器通过回退机制相互通信。
在此示例中,一台服务器在端口 80 上运行,另一台服务器在端口 82 上运行,并且有一个名为 fallback 的 api 端点调用 hello 备用服务器端点。
完整的解决方案可以在这里找到:https://github.com/diogonborges/integration-test-communicating-servers
public class Tests
{
private HttpClient _port80Client;
private HttpClient _port82Client;
[SetUp]
public void Setup()
{
// Create application factories for master and utility services and corresponding HTTP clients
var port80Factory = new CustomWebApplicationFactory(80, 82);
_port80Client = port80Factory.CreateClient();
port80Factory.Server.Features.Set<IServerAddressesFeature>(new ServerAddressesFeature {Addresses = {"http://localhost:80"}});
var port82Factory = new CustomWebApplicationFactory(82, 80);
_port82Client = port82Factory.CreateClient();
port82Factory.Server.Features.Set<IServerAddressesFeature>(new ServerAddressesFeature {Addresses = {"http://localhost:82"}});
// Mock dependency on utility service by replacing named HTTP client
port80Factory.AddHttpClient(Constants.Fallback, _port82Client);
port82Factory.AddHttpClient(Constants.Fallback, _port80Client);
}
[Test]
public async Task Port80_says_hello()
{
var response = await _port80Client.GetAsync("hello");
var content = await response.Content.ReadAsStringAsync();
Assert.AreEqual("hello from http://localhost:80", content);
}
[Test]
public async Task Port80_falls_back_to_82()
{
var response = await _port80Client.GetAsync("hello/fallback");
var content = await response.Content.ReadAsStringAsync();
Assert.AreEqual("hello from http://localhost:82", content);
}
[Test]
public async Task Port82_says_hello()
{
var response = await _port82Client.GetAsync("hello");
var content = await response.Content.ReadAsStringAsync();
Assert.AreEqual("hello from http://localhost:82", content);
}
[Test]
public async Task Port82_falls_back_to_80()
{
var response = await _port82Client.GetAsync("hello/fallback");
var content = await response.Content.ReadAsStringAsync();
Assert.AreEqual("hello from http://localhost:80", content);
}
}
有人知道是否可以在同一个单元测试中托管 WebApplicationFactory<TStartop>()
的多个实例吗?
我已经尝试过了,但似乎无法解决这个问题。
即
_client = WebHost<Startup>.GetFactory().CreateClient();
var baseUri = PathString.FromUriComponent(_client.BaseAddress);
_url = baseUri.Value;
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", "Y2E890F4-E9AE-468D-8294-6164C59B099Y");
WebHost
只是一个帮手 class 让我可以轻松地在一行中建立工厂然后建立客户。
在幕后它所做的是这样的:
new WebApplicationFactory<TStartup>()
还有其他一些事情。
会很好如果我能建立另一个不同的 web 服务器的实例来测试服务器到服务器的功能。
有谁知道这是否可能?
没有。这是不可能的。 WebApplicationFactory
依靠 xUnit 的 IClassFixture
,它必须在 class 级别应用,这意味着您只能咬一口苹果。 WebApplicationFactory
本身可以根据测试进行自定义,这可以满足大多数需要 "different" 的用例,但它无助于同时需要两个完全独立的活动测试服务器时间.
但是,也就是说,您首先想要的是糟糕的测试设计。测试的全部意义在于消除变量,这样您就可以真正确保 SUT 的一部分确实在工作。即使在集成测试环境中,您仍然只是在查看应用程序各部分之间的一种特定交互。有两台测试服务器,相互馈送,有效地增加变量,让您无法保证任何一方都在正常工作。
与公认的答案相反,使用两个 WebApplicationFactory
实例测试服务器到服务器的功能实际上非常容易:
public class OrderAPIFactory : WebApplicationFactory<Order>
{
public OrderAPIFactory() { ... }
protected override void ConfigureWebHost(IWebHostBuilder builder) { ... }
}
public class BasketAPIFactory : WebApplicationFactory<BasketStartup>
{
public BasketAPIFactory() { ... }
protected override void ConfigureWebHost(IWebHostBuilder builder) { ... }
}
然后您可以按如下方式实例化自定义工厂:
[Fact]
public async Task TestName()
{
var orderFactory = new OrderAPIFactory();
var basketFactory = new BasketAPIFactory();
var orderHttpClient = orderFactory.CreateClient();
var basketHttpClient = basketFactory.CreateClient();
// you can hit eg an endpoint on either side that triggers server-to-server communication
var orderResponse = await orderHttpClient.GetAsync("api/orders");
var basketResponse = await basketHttpClient.GetAsync("api/basket");
}
我也不同意关于它必然是糟糕设计的公认答案:它有它的用例。我的公司有一个微服务基础设施,它依赖 data duplication across microservices and uses an async messaging queue with integration events 来确保数据的一致性。不用说,消息传递功能起着核心作用,需要进行适当的测试。此处描述的测试设置在这种情况下非常有用。例如,它允许我们彻底测试在发布消息时已关闭的服务如何处理消息:
[Fact]
public async Task DataConsistencyEvents_DependentServiceIsDown_SynchronisesDataWhenUp()
{
var orderFactory = new OrderAPIFactory();
var orderHttpClient = orderFactory.CreateClient();
// a new order is created which leads to a data consistency event being published,
// which is to be consumed by the BasketAPI service
var order = new Order { ... };
await orderHttpClient.PostAsync("api/orders", order);
// we only instantiate the BasketAPI service after the creation of the order
// to mimic downtime. If all goes well, it will still receive the
// message that was delivered to its queue and data consistency is preserved
var basketFactory = new BasketAPIFactory();
var basketHttpClient = orderFactory.CreateClient();
// get the basket with all ordered items included from BasketAPI
var basketResponse = await basketHttpClient.GetAsync("api/baskets?include=orders");
// check if the new order is contained in the payload of BasketAPI
AssertContainsNewOrder(basketResponse, order);
}
可以在单个集成测试中承载 WebApplicationFactory 的多个通信实例。
假设我们有一个名为 WebApplication
的主服务,它依赖于名为 WebService
的实用程序服务,使用名为“WebService”的 HttpClient。
这是集成测试的例子:
[Fact]
public async Task GetWeatherForecast_ShouldReturnSuccessResult()
{
// Create application factories for master and utility services and corresponding HTTP clients
var webApplicationFactory = new CustomWebApplicationFactory();
var webApplicationClient = webApplicationFactory.CreateClient();
var webServiceFactory = new WebApplicationFactory<Startup>();
var webServiceClient = webServiceFactory.CreateClient();
// Mock dependency on utility service by replacing named HTTP client
webApplicationFactory.AddHttpClient(clientName: "WebService", webServiceClient);
// Perform test request
var response = await webApplicationClient.GetAsync("weatherForecast");
// Assert the result
response.EnsureSuccessStatusCode();
var forecast = await response.Content.ReadAsAsync<IEnumerable<WeatherForecast>>();
Assert.Equal(10, forecast.Count());
}
此代码需要 CustomWebApplicationFactory
class 才能实现:
// Extends WebApplicationFactory allowing to replace named HTTP clients
internal sealed class CustomWebApplicationFactory
: WebApplicationFactory<WebApplication.Startup>
{
// Contains replaced named HTTP clients
private ConcurrentDictionary<string, HttpClient> HttpClients { get; } =
new ConcurrentDictionary<string, HttpClient>();
// Add replaced named HTTP client
public void AddHttpClient(string clientName, HttpClient client)
{
if (!HttpClients.TryAdd(clientName, client))
{
throw new InvalidOperationException(
$"HttpClient with name {clientName} is already added");
}
}
// Replaces implementation of standard IHttpClientFactory interface with
// custom one providing replaced HTTP clients from HttpClients dictionary
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.ConfigureServices(services =>
services.AddSingleton<IHttpClientFactory>(
new CustomHttpClientFactory(HttpClients)));
}
}
最后,CustomHttpClientFactory
class 是必需的:
// Implements IHttpClientFactory by providing named HTTP clients
// directly from specified dictionary
internal class CustomHttpClientFactory : IHttpClientFactory
{
// Takes dictionary storing named HTTP clients in constructor
public CustomHttpClientFactory(
IReadOnlyDictionary<string, HttpClient> httpClients)
{
HttpClients = httpClients;
}
private IReadOnlyDictionary<string, HttpClient> HttpClients { get; }
// Provides named HTTP client from dictionary
public HttpClient CreateClient(string name) =>
HttpClients.GetValueOrDefault(name)
?? throw new InvalidOperationException(
$"HTTP client is not found for client with name {name}");
}
您可以在此处找到示例的完整代码:https://github.com/GennadyGS/AspNetCoreIntegrationTesting
这种方法的优点是:
- 能够测试服务之间的交互;
- 无需模拟服务的内部结构,以便您可以将它们视为黑盒;
- 测试对于任何重构(包括通信协议的更改)都是稳定的;
- 测试快速、独立、不需要任何先决条件并给出可预测的结果。
这种方法的主要 缺点 是参与服务(例如不同主要版本的 EFCore)在现实世界场景中可能存在冲突的依赖关系,因为所有服务都在测试中使用在单个进程中 运行。 有几种缓解此类问题的方法。其中之一是将模块化方法应用于服务的实现,并根据配置文件在运行时加载模块。这可能允许在测试中替换配置文件,从加载中排除几个模块,并用更简单的模拟替换缺失的服务。您可以在上面示例存储库的分支“模块化”中找到应用这种方法的示例。
我基于 Gennadii Saltyshchak 的解决方案创建了这个,这正是我一直在寻找的:两台服务器通过回退机制相互通信。
在此示例中,一台服务器在端口 80 上运行,另一台服务器在端口 82 上运行,并且有一个名为 fallback 的 api 端点调用 hello 备用服务器端点。
完整的解决方案可以在这里找到:https://github.com/diogonborges/integration-test-communicating-servers
public class Tests
{
private HttpClient _port80Client;
private HttpClient _port82Client;
[SetUp]
public void Setup()
{
// Create application factories for master and utility services and corresponding HTTP clients
var port80Factory = new CustomWebApplicationFactory(80, 82);
_port80Client = port80Factory.CreateClient();
port80Factory.Server.Features.Set<IServerAddressesFeature>(new ServerAddressesFeature {Addresses = {"http://localhost:80"}});
var port82Factory = new CustomWebApplicationFactory(82, 80);
_port82Client = port82Factory.CreateClient();
port82Factory.Server.Features.Set<IServerAddressesFeature>(new ServerAddressesFeature {Addresses = {"http://localhost:82"}});
// Mock dependency on utility service by replacing named HTTP client
port80Factory.AddHttpClient(Constants.Fallback, _port82Client);
port82Factory.AddHttpClient(Constants.Fallback, _port80Client);
}
[Test]
public async Task Port80_says_hello()
{
var response = await _port80Client.GetAsync("hello");
var content = await response.Content.ReadAsStringAsync();
Assert.AreEqual("hello from http://localhost:80", content);
}
[Test]
public async Task Port80_falls_back_to_82()
{
var response = await _port80Client.GetAsync("hello/fallback");
var content = await response.Content.ReadAsStringAsync();
Assert.AreEqual("hello from http://localhost:82", content);
}
[Test]
public async Task Port82_says_hello()
{
var response = await _port82Client.GetAsync("hello");
var content = await response.Content.ReadAsStringAsync();
Assert.AreEqual("hello from http://localhost:82", content);
}
[Test]
public async Task Port82_falls_back_to_80()
{
var response = await _port82Client.GetAsync("hello/fallback");
var content = await response.Content.ReadAsStringAsync();
Assert.AreEqual("hello from http://localhost:80", content);
}
}