【问题标题】:How to verfiy that a method has been called a certain number of times using Moq?如何使用 Moq 验证方法已被调用一定次数?
【发布时间】:2016-04-10 09:11:29
【问题描述】:

我有以下实现,

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

    }
}

我对使用起订量还很陌生,所以目前还不太了解。

【问题讨论】:

标签: c# unit-testing nunit moq


【解决方案1】:

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

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


方法 1 - 分班

创建独立的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);
}

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

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

【讨论】:

    【解决方案2】:

    正如@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));
    }
    

    我不太喜欢模拟被测系统(这通常是你做错了什么的好迹象),但它确实允许你做你想做的事。

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

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

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

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

    【讨论】:

      【解决方案3】:

      在你的测试课中,

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

      这将适用于您的情况。同样,你必须修改你的每一个函数,因为在模拟中你不能实例化你的类的对象,如果它实现了一个接口请参阅我已经为接口制作了模拟对象

      要了解有关起订量的更多信息,请访问here

      【讨论】:

      • 你也可以在 Moq 上 Times 课。这完全取决于你想怎么走。我已经为您提供了使用 Moq 概念的清晰见解。您也可以寻找此处发布的其他答案。谢谢。
      • 这是错误的 - 在 IMath 的模拟中你没有 CMath 的实现,所以你没有调用 Multiply 的代码。
      • 是的,你是对的@aershov 这就是我问 pallab pain 的问题。我也给了他通用的方法来计算对方法的调用。
      • 谢谢@Mohit,但我在您的解决方案中没有看到对 Factorial() 方法的任何调用。它如何满足Multiply is called 4 times when Factorial of 4 is performed的测试方法条件?
      • 对不起@pallab 痛苦我觉得你的问题有点错误我以为你想计算对乘法函数的调用。另一种解决方案适合您的情况。
      猜你喜欢
      • 2011-04-13
      • 2012-02-26
      • 2013-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多