我正在尝试创建一组(大部分)统一的集成测试,这些测试可以针对从
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));
}
}
真的,我有点困惑......关于如何实现这一点有什么想法吗?
问题是由
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(...));
沿着这些思路做一些事情会起作用。
我创建了一个自定义的
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。
假设您有两个 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();
}