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

时间:2019-02-18 14:08:12

标签: asp.net-mvc asp.net-core .net-core integration-testing xunit

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

2 个答案:

答案 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提供基地址,以实现额外的抽象/可测试性层。最后,您应该将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,因为该泛型类型参数对应于应用程序入口点,而不是实际使用的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
    {

    }