我将如何对这种方法进行单元测试?

How would I unit test this method?

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

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

我还有一个 Get() 方法,其中:

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

是否可以为这样的方法编写测试?我知道我可以模拟依赖项,将它们传递给构造函数并使用 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;
    }
}

除非您使用 MockBehaviour.Loose,否则您必须为每个呼叫创建一个设置。这在你的场景中是有意义的,因为你需要通过模型提供数据,这样你就可以对方法应该做什么做出有意义的断言 return.

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

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

为了测试,最好 method/component 做的事情越少越好。理想情况下是一件事。

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

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

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