AspNetCore集成测试多个WebApplicationFactory实例?

12

有没有人知道在同一个单元测试中是否可以托管多个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只是一个帮助类,让我能够轻松地一行内构建工厂和客户端。

在内部,它所做的就是这样:

new WebApplicationFactory<TStartup>(),但也有其他一些事情。

如果我能够启动另一个不同的Web服务器实例来测试服务器功能,那就太好了

是否有人知道这是否可能?


1
我认为你应该把被接受的答案改成正确的那个,而在这种情况下,就是得票最多的那个(我的)。 - undefined
4个回答

29
与接受的答案所述相反,使用两个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");
}

我不同意有关必然是糟糕设计的被接受答案:它有其用例。我的公司拥有一个基于微服务的基础架构,该架构依赖于跨微服务的数据复制并使用带有集成事件的异步消息队列以确保数据一致性。不用说,消息功能起着核心作用,需要进行适当的测试。在这种情况下,描述的测试设置非常有用。例如,它允许我们彻底测试那些在发布这些消息时处于停机状态的服务如何处理消息:
[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); 
}

5
这种情况是否包括一个服务器调用另一个服务器?就像代理与应用服务器通信一样?例如API密钥检查? - IbrarMumtaz
这取决于您想使用哪种类型的通信。Http请求可能无法直接与WebApplicationFactory配合使用,因为我认为它没有配置Kestrel,但是查看github上的相关问题,您肯定也可以让它正常工作。 - Maurits Moeys
我尝试了你在这里做的事情,但是当实例化WebApplicationFactory的子类时,ConfigureWebHost从未被触发。这意味着我的服务器实际上没有构建,因此它会很快失败。 - Frank Hale
@FrankHale 我需要检查你的代码才能确定出错的原因,但我们已经在我们的情况下使其工作了,这种情况非常相似! :) - Maurits Moeys

7

在单个集成测试中,可以托管多个通信实例的WebApplicationFactory。

假设我们有名为WebApplication的主服务,它使用名为“WebService”的命名HttpClient依赖于名为WebService的实用程序服务。

以下是集成测试示例:

[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类:

// 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类:
// 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 的不同主要版本)在现实世界的场景中,因为所有用于测试的服务都在单个进程中运行。 有几种缓解这个问题的方法。其中之一是对服务实现应用模块化方法,并根据配置文件在运行时加载模块。这可以允许在测试中替换配置文件、排除几个模块的加载并用简单的模拟替换丢失的服务。您可以在上面示例存储库的 "Modular" 分支中找到应用此方法的示例。


1
太棒了!这正是我所需要的。关于如何在测试服务中获取HttpClient的更多细节:在其构造函数中,添加参数IHttpClientFactory? _httpClientFactory = null以便注入到服务中。可选参数确保不必注册IHttpClientFactory(通常在程序以正常模式启动时是这样),但如果已经注册(比如在测试中),可以使用它:HttpClient httpClient = _httpClientFactory?.CreateClient("WebService") ?? new HttpClient(); - Andi

0
我基于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);
    }
}

0
不。这是不可能的。WebApplicationFactory依赖于xUnit的IClassFixture,必须应用于类级别,这意味着你只有一次机会。 WebApplicationFactory本身可以根据测试进行自定义,这可以满足大多数需要“不同”工厂的用例,但它无法帮助你同时想要两个完全独立的活动测试服务器。
然而,话虽如此,你想要的是一个不好的测试设计。测试的整个目的就是消除变量,以便你实际上可以确保SUT的某个部分实际上是有效的。即使在集成测试环境中,你仍然只看到应用程序中一次特定的交互。拥有两个相互关联的测试服务器实际上增加了变量,使你无法确信两侧都正常工作。

好的 - 我有两个 WebAPI,其中一个充当另一个的门卫,并在允许请求通过之前发送查找等请求....因此,本质上我正在同一领域中工作并测试一个功能。我理解你的观点,但对我来说确实需要两个测试服务器。我将不得不诉诸于模拟对其他服务器的调用!这有点困难。 - IbrarMumtaz
1
这并不需要同时使用两个服务器。任何依赖于您的“门卫”服务的应用程序都不会在乎它是否代理请求到另一个服务。他们只需要一个响应,而这个响应来自门卫。因此,对于这些测试,只涉及到门卫服务,而不是其他服务。然后,您只需要测试门卫服务本身,以确保它正确地将请求代理到其他服务。在每种情况下,一个单独的测试服务器就足够了。 - Chris Pratt
你好,@ChrisPratt。你在这里做的是将单元测试原则应用于集成测试。你建议的隔离正是单元测试的目的,而不是集成测试的目的。集成测试是有意针对底层资源进行测试的,为什么不能适用于其他服务呢? - Jeff Fischer
@jefffischer:不正确。集成测试是确切地测试各个单元之间的集成。您所描述的是系统测试,完全不同。 - Chris Pratt
我明白你的意思,但是根据我的经验,在我所在的每个团队中,“集成测试”的内涵几乎都被“功能测试”所取代。你是说你们的团队不使用“集成测试”来表示“功能测试”吗?虽然我不同意这种术语,但这是我见过的所有情况,@ChrisPratt。 - Jeff Fischer
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接