为什么当 Moq 的 Strict 行为抛出在测试代码下的系统中捕获的异常时,xUnit 测试会通过?

问题描述 投票:0回答:1

我发现 xUnit 和 Moq 的 Strict 行为存在一个微妙但重要的问题,可能会导致假阳性测试结果。这是场景:

  1. 测试设置
  • 将 Moq 与 MockBehavior.Strict 结合使用
  • 被测系统中的多个模拟依赖项
  • 被测系统中的异常处理
  • 测试失败场景
  1. 问题
  • 第一个模拟服务没有设置,但行为严格
  • 第二个模拟服务设置为抛出预期异常
  • 被测系统有 try-catch 块
  • 测试预计第二次服务失败会出现 503
  1. 实际发生了什么
  • 第一个服务抛出起订量异常(由于严格+无设置)
  • 被测系统捕获异常
  • 返回503状态码
  • 第二个服务从未呼叫过
  • 测试通过,但原因错误
  1. 为什么这是有问题的
  • 测试通过但验证错误行为
  • Moq 的严格验证被有效绕过
  • 生产错误处理掩盖了模拟验证失败
  • 可以隐藏丢失的模拟设置
  • 使测试无法可靠地捕获集成问题
  1. 验证
  • 模拟调用计数显示意外的流程
  • 异常堆栈跟踪揭示了错误的异常源
  • 第二个模拟从未执行过
  • 测试输出显示严格违规警告

这是一个最小的复制品:

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 的严格行为时测试失败?

exception moq xunit strict false-positive
1个回答
0
投票

我不确定我是否理解为什么您认为测试应该以任何其他方式表现。正如您在评论中所描述的,

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,以便您可以观察到测试按预期失败。

© www.soinside.com 2019 - 2024. All rights reserved.