我发现 xUnit 和 Moq 的 Strict 行为存在一个微妙但重要的问题,可能会导致假阳性测试结果。这是场景:
这是一个最小的复制品:
public class CoreService
{
private readonly IFirstService _first;
private readonly ISecondService _second;
public async Task<IActionResult> Process(Request request)
{
try
{
var firstResult = await _first.Handle(request); // Throws Moq exception
var secondResult = await _second.Process(firstResult); // Never reached
return new OkResult();
}
catch (Exception)
{
return new StatusCodeResult(503);
}
}
}
public class CoreServiceTests
{
[Fact]
public async Task Process_WhenSecondServiceFails_Returns503()
{
// Arrange
var firstService = new Mock<IFirstService>(MockBehavior.Strict);
// No setup -> will throw MockException
var secondService = new Mock<ISecondService>();
secondService.Setup(x => x.Process(It.IsAny<Stream>()))
.ThrowsAsync(new Exception("Expected failure"));
var sut = new CoreService(firstService.Object, secondService.Object);
// Act
var result = await sut.Process(new Request());
// Assert
Assert.Equal(503, (result as StatusCodeResult).StatusCode);
// Test PASSES but for wrong reason:
// - Gets 503 from firstService MockException
// - secondService never called
// - Moq's Strict validation silently fails
}
}
测试通过是因为生产代码捕获了所有异常,包括 Moq 的 Strict 行为异常。这掩盖了模拟验证失败并且测试因错误原因成功。
处理这种情况的正确方法是什么,以确保在违反 Moq 的严格行为时测试失败?
我不确定我是否理解为什么您认为测试应该以任何其他方式表现。正如您在评论中所描述的,
firstService
配置为严格,这意味着当(意外)调用 Handle
时,它将引发异常。
因为异常处理程序捕获任何异常,所以
CoreService.Process
永远不会通过对 Handle
的调用。 (正如 Damien_The_Un believer 在评论中所写,不要抓住 System.Exception
。有很多文献详细介绍了为什么你永远不想这样做。)
更让我困惑的是为什么你配置了
firstService
严格的行为,而不是 secondService
。如果这是您想要测试的服务,您可能需要考虑使该服务严格,而第一个服务不严格。
我也不清楚为什么你希望这个测试来验证当
secondService
抛出异常时会发生什么,即使测试从不检查是否发生了异常。
我能想到的可能解决这些问题的最小改变是这样的:
[Fact]
public async Task Process_WhenSecondServiceFails_Returns503()
{
// Arrange
var firstService = new Mock<IFirstService>(MockBehavior.Strict);
firstService.Setup(x => x.Handle(It.IsAny<Request>()))
.ReturnsAsync(new MemoryStream());
var secondService = new Mock<ISecondService>();
secondService.Setup(x => x.Process(It.IsAny<Stream>()))
.ThrowsAsync(new Exception("Expected failure"));
var sut = new CoreService(firstService.Object, secondService.Object);
// Act
var result = await sut.Process(new Request());
// Assert
Assert.Equal(503, (result as StatusCodeResult).StatusCode);
secondService.VerifyAll();
}
通过显式配置
firstService
的行为,您可以使 Handle
继续经过 CoreService.Process
。通过调用 _first.Handle
,您可以显式检查是否调用了
secondService.VerifyAll()
。综上所述,我建议不要使用严格的模拟。您还可以在互联网上搜索以了解为什么严格模拟是一个坏主意。关于这一点也有很多文献,但其要点是它使您的测试比应有的更加脆弱。
我可能只是在这里猜测,但我感觉到这个具体问题背后是一个更普遍的问题:
简短的回答是:
看到测试失败。最好的方法是在被测系统 (SUT) 之前编写测试。具体来说,红绿重构循环包括一个步骤“验证测试是否按预期失败”。只有当您看到测试因正确原因失败时,您才会继续使测试通过。
看到测试失败很重要,主要是为了防止以“同义反复断言”形式出现误报。 如果 SUT 已经存在,并且您必须在事后添加测试怎么办?
在这种情况下,您需要遵循特性测试的流程。如果做得正确,此过程还应包括一个步骤,您可以故意暂时破坏 SUT,以便您可以观察到测试按预期失败。