我有一个带动作功能的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控制器进行外部调用时的响应。 谢谢您的帮助。
答案 0 :(得分:1)
问题是您有一个隐藏的依赖项,即HttpClient
。因为您要在操作中进行更新,所以无法嘲笑。相反,您应该将此依赖项注入到控制器中。借助HttpClient
,使用ASP.NET Core 2.1+可以实现IHttpClientFactory
。但是,由于控制器未在服务集合中注册,因此无法直接将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");
可以(可能应该)通过config提供基地址,以实现额外的抽象/可测试性层。最后,您应该将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
,因为该泛型类型参数对应于应用程序入口点,而不是实际使用的Startup
类。
答案 1 :(得分:-1)
我认为最好的方法-我使用界面和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
{
}