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

How to verfiy that a method has been called a certain number of times using 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;
    }
}

如何测试在计算 n 的阶乘时 Multiply() 被调用 n 次?

我正在使用 NUnit 3 和最小起订量。以下是我已经编写的测试,

[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() {

    }
}

我对使用 Moq 还很陌生,所以我现在不太了解它。

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

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


方法 1 - 拆分 classes

创建单独的阶乘class,这样乘法将在单独的界面中。

 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);
}

并且在您的测试中,您可以创建 IMath 的 Mock

[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));
}

方法 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));
}

在你的测试中 class ,

using Math.Library;
using System;
using Moq;
using NUnit.Framework;

namespace UnitTests {
[TestFixture]
public class CMathTests {

    CMath mathObj;
    private IMath _math;
    [SetUp]
    public void Setup() {
        mathObj = new CMath();// no need for this in mocking and its a wrong      approach
        _math = new Mock<IMath>();//initialize a mock object
    }

    [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() {
    int count = 0;
    _math.setup(x =>x.Multiply(It.IsAny<Int>(),It.IsAny<Int>())).Callback(() => count++);
    _math.verify(x =>x.Multiply(),"Multiply is called"+ count+" number of times");
    }
}
}

这将适合您的情况。同样,你必须修改你的每个函数,因为在模拟中你不能实例化你的 class 的对象,如果它实现了一个接口 看到我已经为接口 .

要了解有关 Moq 的更多信息,请访问 here

正如@aershov 所说,您不需要测试 Multiply 是否已被调用 4 次。这是一个实现细节,您已经在测试中进行了一定程度的测试 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));
}

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

实施

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

测试

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));
}

我不太喜欢模拟被测系统(这通常是您做错事的好兆头),但它确实允许您按照您的要求去做。

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

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

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

事实上,测试实际上会增加摩擦,因为更难更改 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;
}

阶乘方​​法的 input/output 测试将继续工作,但是计算对 Multiply 的调用次数的 Mocked 测试会中断,因为只需要 3 次调用就可以计算出答案.

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