【问题标题】:Test controller action that makes an external API call进行外部 API 调用的测试控制器操作
【发布时间】:2019-02-18 14:08:11
【问题描述】:

我有一个带有动作功能的 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
        }
    }
}

这是我要使用的测试类

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


    【解决方案1】:

    问题是你有一个隐藏的依赖,即HttpClient。因为你在你的行动中更新了这一点,所以不可能嘲笑。相反,您应该将此依赖项注入您的控制器。借助 IHttpClientFactory,有了 ASP.NET Core 2.1+,HttpClient 成为可能。但是,开箱即用,您不能将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。它使切换服务和其他实现变得更加容易,而无需重写 Startup 中的所有 ConfigureServices 逻辑,这当然会为您的测试添加变量:例如是不是因为我忘记用与真实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 类。

    【讨论】:

      【解决方案2】:

      我认为最好的方法 - 我使用接口和 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
          {
      
          }
      

      【讨论】:

        猜你喜欢
        • 2020-05-22
        • 1970-01-01
        • 2022-12-05
        • 2015-07-16
        • 2020-09-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-01-28
        相关资源
        最近更新 更多