我有一个带有动作功能的API控制器。此函数对另一个API进行外部调用以获取一些数据。只需使用URL创建客户端即可进行此外部调用。我想使用WebApplicationFactory创建一个测试来测试这个动作函数。我想知道如何配置此外部呼叫。要说服务器是否调用此URL,请返回此响应。
也许它应该在覆盖ConfigureWebHost的某个地方告诉服务器如果你调用这个URL(外部API url)返回这个响应。
这是我想要测试的控制器动作。
namespace MyAppAPI.Controllers
{
public class MyController : ControllerBase
{
[HttpPost("MyAction")]
public async Task MyAction([FromBody] int inputParam)
{
var externalApiURL = "http://www.external.com?param=inputParam";
var client = new HttpClient();
var externalResponse = await client.GetAsync(externalApiURL);
//more work with the externalResponse
}
}
}
这是我想要使用的Test类
public class MyAppAPITests : IClassFixture<WebApplicationFactory<MyAppAPI.Startup>>
{
private readonly WebApplicationFactory<MyAppAPI.Startup> _factory;
public MyAppAPITests(WebApplicationFactory<MyAppAPI.Startup> factory)
{
_factory = factory;
}
[Fact]
public async Task Test_MyActionReturnsExpectedResponse()
{
//Arrange Code
//Act
//Here I would like to have something like this or a similar fashion
_factory.ConfigureReponseForURL("http://www.external.com?param=inputParam",
response => {
response.Response = "ExpectedResponse";
});
//Assert Code
}
}
Test_MyActionReturnsExpectedResponse中的代码在任何地方都不存在,这正是我希望通过继承WebApplicationFactory或通过配置它来实现的。我想知道如何实现这一目标。即,当API控制器进行外部呼叫时配置响应。谢谢您的帮助。
问题是你有一个隐藏的依赖,即HttpClient
。因为你在行动中对此进行了新的修改,所以不可能嘲笑。相反,您应该将此依赖项注入控制器。借助HttpClient
,使用IHttpClientFactory
可以实现ASP.NET Core 2.1+。但是,开箱即用,您无法将HttpClient
直接注入控制器,因为控制器未在服务集合中注册。虽然您可以更改它,但建议的方法是创建“服务”类。这实际上更好,因为它完全从控制器中抽象出与此API交互的知识。长短,你应该做的事情如下:
public class ExternalApiService
{
private readonly HttpClient _httpClient;
public ExternalApiService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public Task<ExternalReponseType> GetExternalResponseAsync(int inputParam) =>
_httpClient.GetAsync($"/endpoint?param={inputParam}");
}
然后,在ConfigureServices
注册:
services.AddHttpClient<ExternalApiService>(c =>
{
c.BaseAddress = new Uri("http://www.external.com");
});
最后,将它注入您的控制器:
public class MyController : ControllerBase
{
private readonly ExternalApiService _externalApi;
public MyController(ExternalApiService externalApi)
{
_externalApi = externalApi;
}
[HttpPost("MyAction")]
public async Task MyAction([FromBody] int inputParam)
{
var externalResponse = await _externalApi.GetExternalResponseAsync(inputParam);
//more work with the externalResponse
}
}
现在,使用此API的逻辑从控制器中抽象出来,并且您可以轻松地模拟依赖项。由于您希望进行集成测试,因此在测试时您需要在不同的服务实现中进行子操作。为此,我实际上做了一些进一步的抽象。首先,为ExternalApiService
创建一个接口并使服务器实现该接口。然后,在您的测试项目中,您可以创建一个替代实现,完全绕过HttpClient
并返回预先生成的响应。然后,虽然不是绝对必要,但我会创建一个IServiceCollection
扩展来抽象AddHttpClient
调用,允许您重用此逻辑而不重复自己:
public static class IServiceCollectionExtensions
{
public static IServiceCollection AddExternalApiService<TImplementation>(this IServiceCollection services, string baseAddress)
where TImplementation : class, IExternalApiService
{
services.AddHttpClient<IExternalApiService, TImplementation>(c =>
{
c.BaseAddress = new Uri(baseAddress)
});
return services;
}
}
您将使用哪个:
services.AddExternalApiService<ExternalApiService>("http://www.external.com");
基地址可以(也可能应该)通过配置提供,以获得额外的抽象/可测试性层。最后,你应该使用TestStartup
与WebApplicationFactory
。如果不重写ConfigureServices
中的所有Startup
逻辑,它可以更容易地切换服务和其他实现,这当然会为您的测试添加变量:例如它不工作,因为我忘了注册与我真正的Startup
相同的方式?
只需在Startup
类中添加一些虚拟方法,然后将其用于添加数据库等操作,并在此处添加您的服务:
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
AddExternalApiService(services);
}
protected virtual void AddExternalApiService(IServiceCollection services)
{
services.AddExternalApiService<ExternalApiService>("http://www.external.com");
}
}
然后,在您的测试项目中,您可以从Startup
派生并覆盖此类似方法:
public class TestStartup : MyAppAPI.Startup
{
protected override void AddExternalApiService(IServiceCollection services)
{
// sub in your test `IExternalApiService` implementation
services.AddExternalApiService<TestExternalApiService>("http://www.external.com");
}
}
最后,在获得测试客户端时:
var client = _factory.WithWebHostBuilder(b => b.UseStartup<TestStartup>()).CreateClient();
实际的WebApplicationFactory
仍然使用MyAppAPI.Startup
,因为该泛型类型param对应于app入口点,而不是实际使用的Startup
类。
我认为最好的方法 - 我使用接口和MOCK。通过继承HttpClient实现接口,并在测试模拟此接口:
public interface IHttpClientMockable
{
Task<string> GetStringAsync(string requestUri);
Task<string> GetStringAsync(Uri requestUri);
Task<byte[]> GetByteArrayAsync(string requestUri);
Task<byte[]> GetByteArrayAsync(Uri requestUri);
Task<Stream> GetStreamAsync(string requestUri);
Task<Stream> GetStreamAsync(Uri requestUri);
Task<HttpResponseMessage> GetAsync(string requestUri);
Task<HttpResponseMessage> GetAsync(Uri requestUri);
Task<HttpResponseMessage> GetAsync(string requestUri, HttpCompletionOption completionOption);
Task<HttpResponseMessage> GetAsync(Uri requestUri, HttpCompletionOption completionOption);
Task<HttpResponseMessage> GetAsync(string requestUri, CancellationToken cancellationToken);
Task<HttpResponseMessage> GetAsync(Uri requestUri, CancellationToken cancellationToken);
Task<HttpResponseMessage> GetAsync(string requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken);
Task<HttpResponseMessage> GetAsync(Uri requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken);
Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content);
Task<HttpResponseMessage> PostAsync(Uri requestUri, HttpContent content);
Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content, CancellationToken cancellationToken);
Task<HttpResponseMessage> PostAsync(Uri requestUri, HttpContent content, CancellationToken cancellationToken);
Task<HttpResponseMessage> PutAsync(string requestUri, HttpContent content);
Task<HttpResponseMessage> PutAsync(Uri requestUri, HttpContent content);
Task<HttpResponseMessage> PutAsync(string requestUri, HttpContent content, CancellationToken cancellationToken);
Task<HttpResponseMessage> PutAsync(Uri requestUri, HttpContent content, CancellationToken cancellationToken);
Task<HttpResponseMessage> DeleteAsync(string requestUri);
Task<HttpResponseMessage> DeleteAsync(Uri requestUri);
Task<HttpResponseMessage> DeleteAsync(string requestUri, CancellationToken cancellationToken);
Task<HttpResponseMessage> DeleteAsync(Uri requestUri, CancellationToken cancellationToken);
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request);
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption);
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken);
void CancelPendingRequests();
HttpRequestHeaders DefaultRequestHeaders { get; }
Uri BaseAddress { get; set; }
TimeSpan Timeout { get; set; }
long MaxResponseContentBufferSize { get; set; }
void Dispose();
}
public class HttpClientMockable: HttpClient, IHttpClientMockable
{
}