测试控制器操作,进行外部API调用

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

我有一个带有动作功能的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控制器进行外部呼叫时配置响应。谢谢您的帮助。

asp.net-mvc asp.net-core .net-core integration-testing xunit
2个回答
1
投票

问题是你有一个隐藏的依赖,即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");

基地址可以(也可能应该)通过配置提供,以获得额外的抽象/可测试性层。最后,你应该使用TestStartupWebApplicationFactory。如果不重写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类。


-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
    {

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