我将如何对这种方法进行单元测试?
How would I unit test this method?
我是单元测试 (xUnit) 的新手,我不太确定在为它编写测试时如何处理这个方法。
基本上我有一个名为 Label.cs 的 class 并且在构造函数中我使用 DI 注入 2 个接口。
我还有一个 Get() 方法,其中:
- 构造一个XML请求
- 删除所有无效字符
- 调用 API
- 从响应中读取标签信息
- 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 完全独立于 Label
s 或 CreateShipmentResponse
的获取方式,您的设计还有一些问题。如果您向 Label
添加一个依赖项并且突然不得不返工数百个不相关的测试,这会变得特别不愉快。将与一个概念远程相关的所有内容都放入一个 class 的想法会随着时间的推移导致 5+kLoC 遗留怪物,这些怪物极难更改和测试。但同样,我不详细了解您的系统,所以这只是供您考虑。对我来说,拥有一个采用 int
、ICarrierService
和 ILabelValidation
的构造函数似乎很奇怪,因为这三个东西可能具有完全不同的生命周期和抽象级别。
我是单元测试 (xUnit) 的新手,我不太确定在为它编写测试时如何处理这个方法。
基本上我有一个名为 Label.cs 的 class 并且在构造函数中我使用 DI 注入 2 个接口。
我还有一个 Get() 方法,其中:
- 构造一个XML请求
- 删除所有无效字符
- 调用 API
- 从响应中读取标签信息
- 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 完全独立于 Label
s 或 CreateShipmentResponse
的获取方式,您的设计还有一些问题。如果您向 Label
添加一个依赖项并且突然不得不返工数百个不相关的测试,这会变得特别不愉快。将与一个概念远程相关的所有内容都放入一个 class 的想法会随着时间的推移导致 5+kLoC 遗留怪物,这些怪物极难更改和测试。但同样,我不详细了解您的系统,所以这只是供您考虑。对我来说,拥有一个采用 int
、ICarrierService
和 ILabelValidation
的构造函数似乎很奇怪,因为这三个东西可能具有完全不同的生命周期和抽象级别。