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

时间:2021-07-03 18:11:10

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

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

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

我正在制作一个围绕 RestEase 构建的小型 HTTP 客户端库。 RestEase 很不错且易于使用,但我无法模拟用于单元测试的调用。

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

IBrandFolderApi - 瑞易发送呼叫所需的接口

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

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

3 个答案:

答案 0 :(得分:1)

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

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

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

答案 1 :(得分: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 抽象,应用需要的任何 3rd 方依赖项

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

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

答案 2 :(得分:0)

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