如何使用Moq验证方法被调用了一定次数?

Pal*_*ain 5 c# nunit unit-testing moq

我有以下实现,

public interface IMath {
    double Add(double a, double b);
    double Subtract(double a, double b);
    double Divide(double a, double b);
    double Multiply(double a, double b);
    double Factorial(int a);
}

public class CMath: IMath {
    public double Add(double a, double b) {
        return a + b;
    }

    public double Subtract(double a, double b) {
        return a - b;
    }

    public double Multiply(double a, double b) {
        return a * b;
    }

    public double Divide(double a, double b) {
        if (b == 0)
            throw new DivideByZeroException();
        return a / b;
    }

    public double Factorial(int a) {
        double factorial = 1.0;
        for (int i = 1; i <= a; i++)
            factorial = Multiply(factorial, i);
        return factorial;
    }
}
Run Code Online (Sandbox Code Playgroud)

我怎样才能测试Multiply()被称为ň时候ň的阶乘是如何计算的?

我正在使用NUnit 3和Moq.以下是我已经写过的测试,

[TestFixture]
public class CMathTests {

    CMath mathObj;

    [SetUp]
    public void Setup() {
        mathObj = new CMath();
    }

    [Test]
    public void Add_Numbers9and5_Expected14() {
        Assert.AreEqual(14, mathObj.Add(9, 5));
    }

    [Test]
    public void Subtract_5From9_Expected4() {
        Assert.AreEqual(4, mathObj.Subtract(9, 5));
    }

    [Test]
    public void Multiply_5by9_Expected45() {
        Assert.AreEqual(45, mathObj.Multiply(5, 9));
    }

    [Test]
    public void When80isDividedby16_ResultIs5() {
        Assert.AreEqual(5, mathObj.Divide(80, 16));
    }

    [Test]
    public void When5isDividedBy0_ExceptionIsThrown() {
        Assert.That(() => mathObj.Divide(1, 0),
            Throws.Exception.TypeOf<DivideByZeroException>());
    }

    [Test]
    public void Factorial_Of4_ShouldReturn24() {
        Assert.That(mathObj.Factorial(4), Is.EqualTo(24));
    }

    [Test]
    public void Factorial_Of4_CallsMultiply4Times() {

    }
}
Run Code Online (Sandbox Code Playgroud)

我对使用Moq相当新,所以我现在还不太了解它.

aer*_*hov 6

你需要分离模拟部分和测试部分,因为Moq是关于消除依赖关系,而你的CMath类没有它们!

但基本上你不需要测试,Multiply被调用4次 - 它是内部实现.检测结果 :)


方法1 - 拆分类

创建Separate Factorial类,以便乘法将在单独的界面中.

 public interface IMath {
        double Add(double a, double b);
        double Subtract(double a, double b);
        double Divide(double a, double b);
        double Multiply(double a, double b);      
    }
 public interface IFactorial {
       double Factorial(int a, IMath math);
}
Run Code Online (Sandbox Code Playgroud)

在您的测试中,您可以创建模拟Math

[Test]
public void Factorial_Of4_CallsMultiply4Times()
{
    var mathMock = new Mock<IMath>();
    var factorial = new Factorial();
    factorial.Factorial(4, mathMock.Object);
    mathMock.Verify(x => x.Multiply(It.IsAny<double>()), Times.Exactly(4));
}
Run Code Online (Sandbox Code Playgroud)

方法2 - 可选的注入代表

public double Factorial(int a, Func<double,double,double> multiply = null)
{
    multiply = multiply ?? CMath.Multiply;
    double factorial = 1.0;
    for (int i = 1; i <= a; i++)
        factorial = multiply(factorial, i);
    return factorial;
}


[Test]
public void Factorial_Of4_CallsMultiply4Times()
{
    var mathMock = new Mock<IMath>();
    var math = new CMath();
    math.Factorial(4, mathMock.Object.Multiply);
    mathMock.Verify(x => x.Multiply(It.IsAny<double>()), Times.Exactly(4));
}
Run Code Online (Sandbox Code Playgroud)


for*_*rir 5

正如@aershov 已经说过的那样,您不需要测试是否Multiply已被调用 4 次。这是一个实现细节,您已经在 test 中进行了一定程度的测试Factorial_Of4_ShouldReturn24。您可能需要考虑使用该TestCase属性来为测试提供一系列输入,而不是单个值:

[TestCase(4, 24)]
[TestCase(2, 2)]
[TestCase(1, 1)]
[TestCase(0, 1)]
public void Factorial_OfInput_ShouldReturnExpected(int input, int expectedResult)
{
    Assert.That(mathObj.Factorial(input), Is.EqualTo(expectedResult));
}
Run Code Online (Sandbox Code Playgroud)

@aershov 涵盖了两个设计更改,可以让您模拟您所询问的交互。第三个可以说是影响最小的更改是使您的Multiply方法virtual。这将允许您使用部分模拟来验证交互。更改如下所示:

执行

public class CMath : IMath
{
    public virtual double Multiply(double a, double b)
    {
        return a * b;
    }
    // ...
Run Code Online (Sandbox Code Playgroud)

测试

Mock<CMath> mockedObj;
CMath mathObj;

[SetUp]
public void Setup()
{
    mockedObj = new Mock<CMath>();
    mockedObj.CallBase = true;
    mathObj = mockedObj.Object;
}

[Test]
public void Factorial_Of4_CallsMultiply4Times()
{
    mathObj.Factorial(4);
    mockedObj.Verify(x => x.Multiply(It.IsAny<double>(), 
                                     It.IsAny<double>()), Times.Exactly(4));
}
Run Code Online (Sandbox Code Playgroud)

我不喜欢嘲笑被测系统(这通常是你做错了什么的好兆头),但是它确实允许你做你所要求的。

模拟可能非常有用,但是当您使用它们时,您需要仔细考虑您实际尝试测试的是什么。查看上面的测试,可以满足以下代码:

public double Factorial(int a)
{
    double factorial = 1.0;
    for (int i = 1; i <= a; i++)
        factorial = Multiply(factorial, a);
    return factorial;
}
Run Code Online (Sandbox Code Playgroud)

这段代码有一个严重的错误,它将源参数传递给循环的每次迭代,而不是循环计数器。其结果是非常不同的,但调用的数量是一样的,所以测试还通过。这是一个很好的迹象,表明测试实际上并没有增加价值。

事实上,测试实际上会增加摩擦,因为更难改变Factorial函数的实现。考虑 4! 的示例。所需的计算是4*3*2*1,但是最后一步乘以 1 本质上是一个 NOP,因为n*1=n。考虑到这一点,可以将阶乘方法稍微优化为:

public double Factorial(int a)
{
    double factorial = 1.0;
    for (int i = 2; i <= a; i++)
        factorial = Multiply(factorial, i);
    return factorial;
}
Run Code Online (Sandbox Code Playgroud)

factorial 方法的输入/输出测试将继续工作,但是 Mocked 测试计算Multiply中断的调用次数,因为计算答案只需要 3 次调用。

在决定使用模拟对象时,请始终考虑收益和成本。