如何模拟/单元测试 HTTP 客户端 - restease

How to mock / unit test HTTP Client - restease

tl;dr:我在模拟 restease 时遇到了问题**

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

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

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

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

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

BrandfolderClient.cs - 主要class

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 格式异常。

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

HttpClient来自System.Net.Http,不易mock。

但是,您可以通过传递假 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") };

不确定您如何在 _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

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>();
}

So, I don't know how to properly mock the Restclient so it doesn't send an actual request to an actual 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();
    }
}

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

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

例如:

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"
  }
}

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