AspNetCore 集成测试多个 WebApplicationFactory 实例?

Posted

技术标签:

【中文标题】AspNetCore 集成测试多个 WebApplicationFactory 实例?【英文标题】:AspNetCore Integration Testing Multiple WebApplicationFactory Instances? 【发布时间】:2019-07-23 11:05:09 【问题描述】:

有谁知道是否可以在同一个单元测试中托管多个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&lt;TStartup&gt;() 还有其他一些事情。

会是 很好如果我能站起来另一个不同的网络服务器实例来测试服务器到服务器的功能。

有人知道这是否可行吗?

【问题讨论】:

【参考方案1】:

没有。这是不可能的。 WebApplicationFactory 依赖于 xUnit 的 IClassFixture,它必须在课堂级别应用,这意味着你只能吃一口苹果。 WebApplicationFactory 本身能够针对每个测试进行自定义,这可以满足您需要“不同”测试服务器的大多数用例,但它无助于您同时需要两个完全独立的活动测试服务器。

但是,话虽如此,您首先需要的是一个糟糕的测试设计。测试的全部目的是消除变量,这样您就可以真正确保 SUT 的一部分实际工作。即使在集成测试环境中,您仍然只是查看应用程序各个部分之间的一种特定交互。有两台测试服务器,相互补充,有效地增加变量,让您无法保证任何一方都正常工作。

【讨论】:

很公平 - 我有两个 webapis 一个充当另一个的看门人,并在让请求通过之前发送查找等请求......所以本质上我在同一个域中工作并测试一项功能。我明白你的意思,但对我来说,我确实需要两台测试服务器。我将不得不求助于模拟对另一台服务器的调用!这是一个伸展tbh 这仍然不需要同时使用两台服务器。任何依赖于你的“看门人”服务的应用程序都可能不太关心它是否将请求代理到另一个服务。他们只需要一个来自看门人的回应。因此,这些测试只涉及看门人服务,而不涉及您的其他服务。然后,您所需要的只是对看门人服务本身进行测试,以确保它正确地将请求代理到其他服务。在每种情况下,单个测试服务器都可以。 你好,@ChrisPratt。您在这里所做的是将单元测试原则应用于集成测试。您所建议的隔离正是 unit testing 的用途,而集成测试正是 not 的用途。集成测试是对底层资源的有意测试,为什么这不适用于其他服务? @jefffischer:不正确。集成测试是从字面上测试各个单元之间的集成。您所描述的是系统测试,完全不同。 我明白你在说什么,但据我所知,“集成测试”的含义已被我所参加过的几乎每个团队的功能测试所取代。您是说您的团队不使用集成测试来表示功能测试?虽然我不同意这个术语,但我所见过的就是这些,@ChrisPratt。【参考方案2】:

与接受的答案所述相反,实际上使用两个 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 并使用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); 

【讨论】:

嗯,这种情况是否包括一台服务器调用另一台服务器?就像代理与应用服务器对话?即如 api 密钥检查? 这取决于您要使用的通信类型。 Http 请求可能不适用于 WebApplicationFactory,因为我认为它没有配置 Kestrel,但看看 this related issue on github 你肯定也可以让它工作。 我尝试了您在此处所做的操作,但在实例化 WebApplicationFactory 的子类时,ConfigureWebHost 从未被触发。这意味着我的服务器实际上并没有构建,所以它只是从那里快速失败。 @FrankHale 我必须检查您的代码以查看出现了什么问题,但我们让它在我们的案例中工作,这非常相似! :)【参考方案3】:

可以在单个集成测试中托管 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 类:

// 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)可能会发生冲突的依赖关系,因为在测试中使用的所有服务都在单个进程中运行. 这种问题有几种缓解方法。其中之一是将模块化方法应用于服务的实现并根据配置文件在运行时加载模块。这可能允许在测试中替换配置文件,从加载中排除几个模块并用更简单的模拟替换丢失的服务。您可以在上面示例存储库的“模块化”分支中找到应用这种方法的示例。

【讨论】:

以上是关于AspNetCore 集成测试多个 WebApplicationFactory 实例?的主要内容,如果未能解决你的问题,请参考以下文章

Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException:请求匹配多个端点

Docker在Linux上运行NetCore系列使用私有Nuget与多个本地包引用运行ASPNetCore

Microsoft.AspNetCore.Identity 和 EF Core,在测试旧密码时重置用户密码

第一个Docker 镜像 ASPNETCORE

Docker在Linux上运行NetCore系列使用私有Nuget与多个本地包引用运行ASPNetCore

如何在 DevOps 上针对多个服务运行集成测试?