【问题标题】:How to mock / unit test HTTP Client - restease如何模拟/单元测试 HTTP 客户端 - restease
【发布时间】:2021-07-03 18:11:10
【问题描述】:

tl;dr:我在嘲笑 restease 时遇到了麻烦**

另外,我意识到我可能完全走错了路,所以任何朝着正确方向的建议/推动都会有很大帮助。我对此很陌生。

我正在制作一个小型 HTTP 客户端库,围绕 RestEase 构建。 RestEase 非常好用且易于使用,但我在模拟调用以进行单元测试时遇到了麻烦。

我想使用 moq 和 NUnit,但我无法正确模拟 RestClient。示例(为简洁起见缩短):

IBrandFolderApi - restease 发送调用所需的接口

public interface IBrandFolderApi
{
    [Post("services/apilogin")]
    Task<LoginResponse> Login([Query] string username, [Query] string password);
}

BrandfolderClient.cs - 主类

public class BrandfolderClient : IBrandfolderClient
{
    private IBrandFolderApi _brandFolderApi { get; set; } 

    public BrandfolderClient(string url)
    {
        _brandFolderApi = RestClient.For<IBrandFolderApi >(url);
    }

    public async Task<string> Login(string username, string password)
    {
        LoginResponse loginResponse = await _brandFolderApi .Login(username, password);
        if (loginResponse.LoginSuccess)
        {
            ....
        }
        ....            
        return loginResponse.LoginSuccess.ToString();
    }
}

单元测试

public class BrandFolderTests
{
    BrandfolderClient  _brandfolderClient 
    Mock<IBrandFolderApi> _mockBrandFolderApii;
    
    
    [SetUp]
    public void Setup()
    {
         //The test will fail here, as I'm passing a real URL and it will try and contact it.
        //If I try and send any string, I receive an Invalid URL Format exception.
         string url = "https://brandfolder.companyname.io";
        _brandfolderClient = new BrandfolderClient  (url);
        _mockBrandFolderApii= new Mock<IBrandFolderApi>();
    }

    ....
}

所以,我不知道如何正确模拟 Restclient,因此它不会向实际 URL 发送实际请求。

测试在构造函数中失败 - 如果我发送一个有效的 URL 字符串,那么它将发送一个对实际 URL 的调用。如果我发送任何其他字符串,则会收到无效的 URL 格式异常。

我相信我没有在其余客户端上正确实施某些东西,但我不确定在哪里。我非常坚持这一点,我一直在疯狂地搜索和阅读,但我错过了一些东西,我不知道是什么。

【问题讨论】:

    标签: c# unit-testing .net-core httpclient moq


    【解决方案1】:

    所以,我不知道如何正确模拟 Restclient,因此它不会向实际 URL 发送实际请求。

    你实际上不应该模拟RestClient

    重构您的代码以显式依赖您控制的抽象

    public class BrandfolderClient : IBrandfolderClient {
        private readonly IBrandFolderApi brandFolderApi;
    
        public BrandfolderClient(IBrandFolderApi brandFolderApi) {
            this.brandFolderApi = brandFolderApi; //RestClient.For<IBrandFolderApi >(url);
        }
    
        public async Task<string> Login(string username, string password) {
            LoginResponse loginResponse = await brandFolderApi.Login(username, password);
            if (loginResponse.LoginSuccess) {
                //....
            }
    
            //....
    
            return loginResponse.LoginSuccess.ToString();
        }
    }
    

    消除与静态 3rd 方实现问题的紧密耦合将使您的主题更明确地了解执行其功能的实际需要。

    这也将使受试者更容易被隔离测试。

    例如:

    public class BrandFolderTests { 
        BrandfolderClient subject;
        Mock<IBrandFolderApi> mockBrandFolderApi;
    
        [SetUp]
        public void Setup() {
            mockBrandFolderApi = new Mock<IBrandFolderApi>();
            subject = new BrandfolderClient(mockBrandFolderApi.Object);
        }
    
        //....
    
        [Test]
        public async Task LoginTest() {
            //Arrange
            LoginResponse loginResponse = new LoginResponse() {
                //...
            };
        
            mockBrandFolderApi
                .Setup(x => x.Login(It.IsAny<string>(), It.IsAny<string>()))
                .ReturnsAsync(loginResponse);
    
            //Act        
            string response = await subject.Login("username", "password");
        
            //Assert        
            mockBrandFolderApi.Verify(x => x.Login(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
        }
    }
    

    在生产代码中,向容器注册和配置 IBrandFolderApi 抽象,应用所需的任何第 3 方依赖项

    Startup.ConfigureServices

    //...
    
    ApiOptions apiOptions = Configuration.GetSection("ApiSettings").Get<ApiOptions>();
    services.AddSingleton(apiOptions);
    
    services.AddScoped<IBrandFolderApi>(sp => {
        ApiOptions options = sp.GetService<ApiOptions>();
        string url = options.Url;
        return RestClient.For<IBrandFolderApi>(url);
    });
    

    ApiOptions 用于存储设置的位置

    public class ApiOptions {
        public string Url {get; set;}
        //... any other API specific settings
    }
    

    可以在appsetting.json

    中定义
    {
      ....
    
      "ApiSettings": {
        "Url": "https://brandfolder.companyname.io"
      }
    }
    

    这样它们就不会在你的代码中被硬编码。

    【讨论】:

    • 这是令人难以置信的很好的解释,并且是一种将两者分开的非常干净的方式。我今天确实学到了一些东西。太感谢了!如果您有任何资源/关键字,我可以用谷歌搜索更多关于这些类型的东西,请分享给我和任何未来的读者。
    • @VaCo 我遵循SOLID principles of object-oriented programming 来帮助分离关注点并使代码更易于维护。
    【解决方案2】:

    HttpClient 来自System.Net.Http,不容易模拟。

    但是,您可以通过传递虚假的HttpMessageHandler 来创建测试HttpClient。这是一个例子:

    public class FakeHttpMessageHandler : HttpMessageHandler
    {
        private readonly bool _isSuccessResponse;
    
        public FakeHttpMessageHandler(bool isSuccessResponse = true)
        {
            _isSuccessResponse = isSuccessResponse;
        }
    
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            return Task.FromResult(
                new HttpResponseMessage(_isSuccessResponse ? HttpStatusCode.OK : HttpStatusCode.InternalServerError));
        }
    }
    

    您可以创建一个HttpClient的测试实例,如下所示:

    var httpClient = new HttpClient(new FakeHttpMessageHandler(true))
                { BaseAddress = new Uri("baseUrl") };
    

    【讨论】:

      【解决方案3】:

      不确定您如何在 _httpClient 上使用验证,它不是模拟的。但你要找的是https://github.com/canton7/RestEase#custom-httpclient。大多数人为此通过工厂


      //constructor
      public httpClientConstructor(string url, IHttpHandlerFactory httpHandler)
      {
         var httpClient = new HttpClient(httpHandler.GetHandler())
         {
             BaseAddress = new Uri(url),
         };       
         _exampleApi = RestClient.For<IExampleApi>(url);
      }
      

      public interface IHttpHandlerFactory<T>
      {
         T GetHandler() where T: HttpMessageHandler
      }
      

      感谢 Ankit Vijay https://stackoverflow.com/a/68240316/5963888

      public class FakeHttpMessageHandler : HttpMessageHandler
      {
          private readonly bool _isSuccessResponse;
      
          public FakeHttpMessageHandler(bool isSuccessResponse = true)
          {
              _isSuccessResponse = isSuccessResponse;
          }
      
          protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
          {
              return Task.FromResult(
                  new HttpResponseMessage(_isSuccessResponse ? HttpStatusCode.OK : HttpStatusCode.InternalServerError));
          }
      }
      

      [SetUp]
      public void Setup()
      {
          var fakeHandler = new Mock<IHttpHandlerFactory>();
          fakeHandler.Setup(e => e.GetHandler() ).Returns( new FakeHttpHandler() );
          _httpClient = new HttpClient(fakeHandler.Object);
          _exampleApi = new Mock<IExampleApi>();
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-01-27
        • 2011-06-13
        • 2016-03-27
        • 2018-09-07
        • 1970-01-01
        • 2020-08-05
        • 2021-09-30
        • 1970-01-01
        相关资源
        最近更新 更多