如何使用 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 次调用就可以计算出答案.
在决定使用模拟对象时,请始终考虑收益和成本。
我有以下实现,
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 次调用就可以计算出答案.
在决定使用模拟对象时,请始终考虑收益和成本。