简单突变单元测试错误地报告了异常消息突变的生存情况

Simple mutation unit testing erroneously report survival of the mutation of the exception message

提问人:JamesL 提问时间:6/17/2023 最后编辑:JamesL 更新时间:6/19/2023 访问量:56

问:

假设我们有一个 with a divide 方法,如果分母为 0,则抛出错误Calculator

    public class Calculator
    {
        public double Divide(int x, int y)
        {
            if (y == 0)
            {
                throw new ArgumentOutOfRangeException(paramName: nameof(y), message: CalculatorErrorMessages.DenominatorShouldNotBeZero);
            }

            return (double)x / y;
        }
    }
    public static class CalculatorErrorMessages
    {
        public static string DenominatorShouldNotBeZero = "Denominator should not be zero.";
    }

下面是单元测试,它试图测试异常的引发:

    public class CalculatorUnitTest
    {
        [Fact]
        public void Test1()
        {
            var calculator = new Calculator();

            var ex = Assert.Throws<ArgumentOutOfRangeException>(() => calculator.Divide(1, 0));
            Assert.Contains(CalculatorErrorMessages.DenominatorShouldNotBeZero, ex.Message);
        }
    }

Stryker.NET 报告异常消息在突变后仍然存在:

enter image description here

但这种突变存活并不是一个实际问题,因为单元测试代码和应用代码都以相同的方式生成异常消息。我不想在应用程序和单元测试中复制和维护错误消息

为此,我可以让 Stryker 忽略突变,但这听起来不是一个好主意。Stryker 不会发现单元测试没有像它应该的那样处理错误消息的情况。例如,假设我在 Calculator 中添加了一个新方法,该方法也引发了异常,并且在单元测试中,我没有使用实际的错误消息常量,而是对预期的错误消息进行了硬编码:CalculatorErrorMessagesDivideV2

        [Fact]
        public void Test2()
        {
            var calculator = new Calculator();

            var ex = Assert.Throws<ArgumentOutOfRangeException>(() => calculator.DivideV2(1, 0));
            Assert.Contains("Denominator should not be zero.", ex.Message);
        }

在这种情况下,突变存活确实不容忽视。

我怎样才能以这种方式制作 Stryker:

  • 没有向我展示“假”突变存活
  • 仍然让我的代码单元通过突变进行测试
  • 不要在应用和单元测试中重复错误消息字符串
单元 突变测试 stryker-net

评论

1赞 Dai 6/17/2023
为什么你的领域首先是可变的?你为什么不使用 or ?staticconst Stringstatic readonly String
1赞 Dai 6/17/2023
“我不想在应用和单元测试中复制和维护错误消息。”- 嗯,是的,当然,否则那将是愚蠢的 - 但是对可本地化的人类可读错误消息进行测试也是愚蠢的。测试行为,而不是像可本地化文本这样脆弱的东西(除非特定文本真的很重要,我猜)。
0赞 JamesL 6/17/2023
@Dai我匆匆写了示例代码,但在一个真实的应用程序中,你是对的,它应该是一个 .我编辑了代码示例。const string
1赞 Dai 6/17/2023
“我还能如何测试以确保实际引发了预期的异常?”- 只需检查是否抛出了一个,在这种情况下它是否足够了。ArgumentOutOfRangeExceptionActualValue == 0
1赞 Dai 6/17/2023
“只是试图改变常量”——这没有意义:在 C#/.NET 中不能改变。当您将源字符串更改为 Stryker 时,测试不应报告任何突变(也是不可变的)。你是说即使有?constconstException.Messagepublic const string DenominatorShouldNotBeZero

答:

1赞 Lars 6/17/2023 #1

突变与单元测试

重申其目的:

  • 单元测试用于在应用代码更改时检查代码是否存在错误
  • 突变测试用于检查对代码的更改是否实际上使单元测试陷入困境。它通过修改实际代码并重新运行测试来做到这一点,并期望它们失败。

Stryker 文档解释了哪些突变将应用于代码。
在本例中,a
将更改为 ,预期单元测试失败。
public static string YourVariable = "your-value"public static string YourVariable = ""

计算器示例:

您表达并确定了几个问题:

我不想在应用程序和单元测试中复制和维护错误消息。

这对我来说是完全有道理的。在我看来,朝这个方向前进会让你面临非常脆弱的考验。

忽略 CalculatorErrorMessages 上的突变,但这听起来不是一个好主意。

这也是正确的,原因与您举例的原因相同。 您希望 Mutation 测试在单元测试中对错误消息提供硬编码的期望时将代码标记为已正确测试。

您的目标:

  • Stryker 没有向我展示“假”突变的存活率。是由于突变完成时测试不失败所致。
  • 仍然让我的代码单元通过突变进行测试。是通过不忽略 DenominatorShouldNotBeZero 上的 stryker 突变来完成的
  • 不要在应用和单元测试中重复错误消息字符串。通过编写测试来完成,就像您在第一个示例中所做的那样,并确保突变实际上无法通过您的单元测试。

这就引出了关键,我们必须确保单元测试在突变后失败(也就是杀死突变)。 你可以添加额外的测试和条件,这些测试和条件会在突变后跳闸。

我们在测试什么

我们想要实际的代码:

  • 为了不抛出异常并正确计算除法
  • 在 0 时引发异常
  • 异常类型为 ArgumentOutOfRangeException 类型
  • 使用某些常量作为消息的异常
  • 消息不为空
  • 具有 paramName 的异常(等于第二个参数名称,或者只是不为空)

我们希望单元测试来测试上述所有内容。

突变测试已正确识别出更改当前不会使测试跳闸。 将 const 更改为 '',仍将通过单元测试。
当有人将您的 const 更改为 '' 时,它预计单元测试会失败。
DenominatorShouldNotBeZero

我可以断言前任。Message 不是空字符串。

1赞 psfinaki 6/19/2023 #2

所以,长话短说,实现目标的一种方法是制作字符串。请记住,通过这种方式,您将利用此处描述的 Stryker 限制。因此,不同的突变测试框架或未来的 Stryker 版本仍然可以将其标记为幸存的突变体。const

另外,请记住,这并不比用另一个替换一个“更好”,可能会产生不愉快的后果。你不应该只是为了让史崔克冷静下来而这样做。就你而言,我可能会拥有它.conststaticconststatic readonly

最终,你需要为自己定义测试的级别,你希望它有多黑/白,你希望它有多重言。请记住,您可以忽略特定的突变