【问题标题】:How would I unit test this method?我将如何对这种方法进行单元测试?
【发布时间】:2020-09-29 07:08:14
【问题描述】:

我是单元测试 (xUnit) 的新手,在为它编写测试时我不太确定如何使用此方法。

基本上我有一个名为 Label.cs 的类,在构造函数中我使用 DI 注入 2 个接口。

我还有一个 Get() 方法:

  1. 构造一个 XML 请求
  2. 删除所有无效字符
  3. 调用 API
  4. 从响应中读取标签信息
  5. 返回响应

是否可以为这样的方法编写测试?我知道我可以模拟依赖项,将它们传递给构造函数并使用 Moq 设置方法,但是如果我在 Get() 方法内有 4 或 5 个来自 ILabelValidation 的验证方法怎么办?我需要使用 Moq 设置所有这些方法吗?

public class Label 
{
    private readonly ICarrierService _carrierService;
    private readonly ILabelValidation _validator;

    public Label(int accountId, ICarrierService carrierService, ILabelValidation validator)
    {
        _carrierService = carrierService;
        _validator = validator;
    }

    public override async Task<CreateShipmentResponse> Get(int accountId, Shipment shipment)
    {
        string xmlRequest = ConstructRequest(shipment);
        
        // strip out any invalid characters inside the request
        xmlRequest = _validator.InvalidCharacterValidation(xmlRequest);

        // what if I have another method that I use from the ILabelValidation?
        // _validator.ValidatePackageCount(3);

        var stringContent = new StringContent(xmlRequest, Encoding.UTF8, "text/xml");

        // make the API request
        var shipmentResponse = await _carrierService.CreateShipment(dict, stringContent);

        var stream = await shipmentResponse.Content.ReadAsStreamAsync();
        Models.Label labelInfo = GetLabelInfo(stream);

        var response = new CreateShipmentResponse();
        response.labels = new List<string>();

        if (!string.IsNullOrEmpty(labelInfo.Base64String))
            response.labels.Add(labelInfo.Base64String);

        return response;
    }
}

【问题讨论】:

    标签: c# unit-testing moq xunit


    【解决方案1】:

    正如您自己所指出的,您的方法可以做很多事情。这通常不是一件好事。当您发现难以测试时,这表明您的设计需要更多工作。

    为了测试事物,最好方法/组件做的事情越少越好。理想情况下是一件事。

    显然,在Label 抽象级别上,一件事是Get。但是,当您向下钻取时,您会发现您必须 1. 创建请求,2. 发送请求,3. 处理响应。现在,这些是额外的三个步骤,它们是下面的一个抽象级别,但是是独立的,应该由较低抽象级别的方法表示。现在这些方法可能会更容易测试,但可能需要进一步分解。

    使用这种方法,您可以选择只对Label::Get 进行几个健全性测试,并选择涵盖&lt;Your new request creation component&gt;::CreateRequest 和&lt;Your new response processing component&gt;::ParseResponse 的所有边缘情况的详细测试套件。 Label::Get 测试可能需要一个间谍替身来使用ICarrierService 进行请求验证,而CreateRequest 也可能对ILabelValidation 使用存根。尽管这实际上取决于您的最终设计,这可能与我的假设不同。如果您不确定双打以及它们与起订量的匹配度,您可能需要查看Martin Fowler's Mocks aren't stubs。

    还请注意,像 Label::Get 这样的操作可能看起来非常 OOP 之类的,但是如果您发现自己在使用完全独立于 Labels 对某些业务逻辑实施单元测试时不得不模拟 ICarrierService如何获得Labels 或CreateShipmentResponse,您的设计还有一些问题。如果您向Label 添加一个依赖项并且突然不得不重新处理数百个不相关的测试,这会变得特别不愉快。随着时间的推移,将与一个概念远程相关的所有东西都放在一个类中的想法导致了 5+kLoC 的遗留怪物,这些怪物极难改变和测试。但同样,我不详细了解您的系统,所以这仅供您考虑。对我来说,拥有一个接受 int、ICarrierService 和 ILabelValidation 的构造函数似乎很奇怪,因为这三件事可能具有完全不同的生命周期和抽象级别。

    【讨论】:

      【解决方案2】:

      除非您使用 MockBehaviour.Loose,否则您必须为每个调用创建一个设置。这在您的场景中是有意义的,因为您需要通过模型提供数据,以便您可以对方法应返回的内容做出有意义的断言。

      对于松散的行为,您不需要提供设置,但模拟上的所有方法调用都将返回方法返回类型的默认值 (null),这不适用于您的代码。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-10-17
        • 2017-07-01
        • 1970-01-01
        • 2017-12-19
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多