从WebApplicationFactory注入HttpClient

问题描述 投票:0回答:3

我正在尝试创建一组(大部分)统一的集成测试,这些测试可以针对从

WebApplicationFactory
创建的内存中 API,也可以针对我们应用程序的完全部署版本。使用
XUnit.DependencyInjection
,我计划将
HttpClient
注入到我的测试中,该测试要么指向测试服务器,要么指向基于环境变量的真实应用程序。

因此,要为测试服务器创建客户端,我只需在

Startup.cs
中运行以下命令:

WebApplicationFactory<Program> app = new();
HttpClient client = app.CreateClient();

这似乎有效。但是,我完全不知道如何将

HttpClient
的实现注入到各个测试类中。

这样的东西不起作用(这样的重载不存在):

services.AddHttpClient<MyTestClass>(client);

这也不是(注入的客户端由于某种原因将

BaseAddress
设置为 null):

services.AddHttpClient<InMemoryServerSelfTests>(c =>
                                    {
                                        c.BaseAddress           = client.BaseAddress;
                                        c.Timeout               = client.Timeout;
                                    });

我唯一的另一个想法是创建一个新类来包装两个客户端并注入它,但这看起来很混乱:

public class TestClientWrapper
{
    public readonly HttpClient Client;
    public TestClientWrapper(InMemoryTestServer server)
    {
        Client = server.CreateClient();
    }

    public TestClientWrapper(HttpClient client)
    {
        Client = client;
    }
}

// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    string targetEndpoint = Environment.GetEnvironmentVariable("targetEndpoint"); // Make this configurable
    bool   isLocal        = string.IsNullOrEmpty(targetEndpoint);
    
    if (isLocal)
    {
        InMemoryTestServer app = new();
        services.AddSingleton(new TestClientWrapper(app));
    }
    else
    {
        HttpClient client = new();
        services.AddSingleton(new TestClientWrapper(client));
    }
}

真的,我有点困惑......关于如何实现这一点有什么想法吗?

c# asp.net dependency-injection dotnet-httpclient
3个回答
4
投票

问题是由

HttpClient
生成的
WebApplicationFactory
很特殊,因为
WebApplicationFactory
托管在内存中并且在进程外不可见(我认为这是我在其他地方读到的)。这意味着复制设置不起作用。

我成功注册

WebApplicationFactory
客户端并使其可解析的唯一方法是使用从
IHttpClientFactory
返回客户端的容器注册
WebApplicationFactory
的实例。

class TestClientFactory : IHttpClientFactory
{
    WebApplicationFactory<Startup> _appFactory;

    public TestClientFactory(TestClientFactory _appFactory) => this._appFactory= appFactory;

    public HttpClient CreateClient(string name) => this._appFactory.CreateClient();
}

services.AddSingleton<IHttpClientFactory>(new TestClientFactory(...));

沿着这些思路做一些事情会起作用。


1
投票

我创建了一个自定义的

HttpMessageHandlerBuilder
来解决这个问题。

public class TestServerHttpMessageHandlerBuilder : HttpMessageHandlerBuilder
{
    public TestServerHttpMessageHandlerBuilder(TestServer testServer, IServiceProvider services)
    {
        Services = services;
        PrimaryHandler = testServer.CreateHandler();
    }

    private string? _name;

    [DisallowNull]
    public override string? Name
    {
        get => _name;
        set
        {
            ArgumentNullException.ThrowIfNull(value);
            _name = value;
        }
    }

    public override HttpMessageHandler PrimaryHandler { get; set; }

    public override IList<DelegatingHandler> AdditionalHandlers { get; } = new List<DelegatingHandler>();

    public override IServiceProvider Services { get; }

    public override HttpMessageHandler Build()
    {
        if (PrimaryHandler == null)
        {
            throw new InvalidOperationException($"{nameof(PrimaryHandler)} must not be null");
        }

        return CreateHandlerPipeline(PrimaryHandler, AdditionalHandlers);
    }
}

然后在启动代码中注册自定义

TestServerHttpMessageHandlerBuilder
,然后再调用
AddHttpClient

clientServices.AddTransient<HttpMessageHandlerBuilder>(sp => new TestServerHttpMessageHandlerBuilder(webAppFactory.Server, sp));

AddHttpClient
检查
HttpMessageHandlerBuilder
是否已注册并使用它而不是默认实现。 现在使用
HttpClient
创建的所有
HttpClientFactory
都将使用 TestServer。


0
投票

不同场景:

问题:

假设您有两个 Web/API 应用程序,其中一个应向另一个应用程序发送请求,假设我有一个 FakeApi,我想从我的

Web
应用程序调用它,或者您可能有一个 API 应用程序被 BFF(前端后端)应用程序调用。

Test -> Main application -> FakeApi
Test -> BFF -> API

我们应该如何告诉目标应用程序使用第二个应用程序?

解决方案:

为此,我使我的代码适应了 @satnhak 提供的解决方案,尽管代码有一些问题,所以我也更新了该帖子(如果版主批准)。 所以主要代码是一样的,所以先把功劳归给他吧。

好吧,就我而言,我有 Gherkin/Specflow 测试场景和测试步骤定义。 我的每个 Web 应用程序有两个 WebApplicationFactory 包装器,称为

FakeApiWebApplicationFactory
MainWebApplicationFactory

考虑到我正在

MainWebApplicationFactory
中测试应用程序,它会调用
FakeApiWebApplicationFactory
以获得额外服务。

为此,我使用 @satnhak 代码创建一个

HttpClientFactory
,并将其传递给
MainWebApplicationFactory
,其中我有一个注入
HttpClient
的服务,并使用 :

添加 HttpClient
// Program.cs in Services project
// ---- Program CS should define class if you want to pass it to WebApplicationFactory

builder.Services.AddTransient<IMyService, MyService>();

builder.Services.AddHttpClient<IMyService, MyService>(client =>
{
    client.BaseAddress = new Uri(myServiceSetting.BaseApiUrl);
});

MyService
在自身内部注入
HttpClient
,并使用它连接到假的或真实的 API。

public class MyService: IMyService
{
    private readonly HttpClient _httpClient;

    public GoCardlessBankAccountDataService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
}

这是我的网络工厂:

using FakeApi; // Program.cs in FakeApi project
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;

namespace AcceptanceTests;

public class FakeApiWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        // You can modify your configuration here if needed
        builder.ConfigureServices(services =>
        {
            // E.g., override some services for testing purposes
        });
    }
}
using Services; // Program.cs in Services project
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;

namespace AcceptanceTests;

public class MainWebApplicationFactory : WebApplicationFactory<Program>
{
    private readonly IHttpClientFactory? _fakeApiHttpClientFactory;

    public MainWebApplicationFactory(IHttpClientFactory? fakeApiHttpClientFactory)
    {
        _fakeApiHttpClientFactory = fakeApiHttpClientFactory;
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        if (_fakeApiHttpClientFactory is not null)
        {
            builder.ConfigureServices(services =>
            {
                // E.g., override some services for testing purposes
                services.AddSingleton(_fakeApiHttpClientFactory);
            });
        }
    }
}

在上面的代码中,你可以看到添加了一个条件,我认为是 必要的,因为我可能想在某些测试中使用真实服务,并且我可能想在其他一些测试中覆盖 HttpClientFactory。

现在我按如下方式连接这些类(我再次使用 Specflow,它又使用 XUnit) 这是我的测试文件的开头:

private readonly ScenarioContext _scenarioContext;
    
    private readonly FakeApiWebApplicationFactory _fakeApiFactory;
    private readonly HttpClient _fakeApiClient;
    
    private readonly MainWebApplicationFactory _mainFactory;
    private readonly HttpClient _mainClient;

    public MainServiceIntegrationIntegrationStepDefinitions(ScenarioContext scenarioContext)
    {
        _scenarioContext = scenarioContext;
        _fakeApiFactory = new FakeApiWebApplicationFactory();
        _fakeApiClient = _fakeApiFactory.CreateClient();

        _mainFactory = new MainWebApplicationFactory(new TestHttpClientFactory<Program>(_fakeApiFactory));
        _mainServicesClient = _mainFactory.CreateClient();
    }

如你所见,我首先创建了 FakeApi, 然后使用 @satnhak 代码,我将此工厂传递给主服务。 然后主应用程序使用工厂创建由 FakeApiWebApiFactory 生成的 HttpClient 实例。

注意: 正如我测试的那样,您不需要关心 BaseUrl,因为我的应用程序将 BaseUrl 设置为您已经在

Problem
部分中场景,但由
HttpClient
生成的内存中
WebApplicationFactory
,仍然设法向 FakeApi 发送请求并克服自定义的基本 Url。

TestHttpClientFactory

public class TestHttpClientFactory<TStartup> : IHttpClientFactory 
    where TStartup : class
{
    private readonly WebApplicationFactory<TStartup> _appFactory;

    public TestHttpClientFactory(WebApplicationFactory<TStartup> appFactory) => _appFactory = appFactory;

    public HttpClient CreateClient(string name) => _appFactory.CreateClient();
}

© www.soinside.com 2019 - 2024. All rights reserved.