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

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

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

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

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

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

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

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

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

最佳答案

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

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

关于c# - 如何模拟/单元测试 HTTP 客户端 - restease,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68238847/

相关文章:

c# - 在 c# 中使用两个空白括号后跟箭头的目的是什么

.net - 在.NET Core RC2中构建.exe文件

.net - 如何在 .Net 中使用 Confluence.Kafka 从特定 TopicPartitionOffset 进行消费

c# - WPF 工具包 Datagrid - 如何关闭选择?

c# - Visual Studio,C# 如何将文档文件添加为资源

c# - 我应该模拟还是伪造我的存储库?

html - 自动化视觉布局测试

c# - 当属性值在短窗口内多次更改时,DataTrigger 不会触发

c# - 为已部署的 Windows 应用程序 C# 更新系统?

c# - 在 Azure 表存储中,是否可以使用 .NET Core 查询属性的子字符串?