模拟一个类(如计算器)的目的是什么?

7

我已经使用TDD有一段时间了,但是现在我正在看一些mocking框架,其中有些问题我并不明白。对于有经验的人来说,这个问题可能听起来很愚蠢,但我就是不明白。我使用的库是Moq + xUnit。

问题

如果我明确地声明2+2将返回4,然后再做断言,测试Calculator类的意义是什么?
当然结果将是4,我只是在测试本身的几行上方"强制"它返回4。即使在我的实现中,如果我执行return a * b;而不是return a + b;,测试也将通过。

下面是同一个计算器测试的另一个示例。http://nsubstitute.github.io/

示例代码

namespace UnitTestProject1
{
    using Xunit;
    using Moq;

    public class CalculatorTests
    {
        private readonly ICalculator _calculator;

        public CalculatorTests()
        {
            var mock = new Mock<ICalculator>();

            mock.Setup(x => x.Add(2, 2)).Returns(4);
            mock.Setup(x => x.Subtract(5, 2)).Returns(3);

            this._calculator = mock.Object;
        }

        [Fact]
        public void Calculator_Should_Add()
        {
            var result = _calculator.Add(2, 2);

            Assert.Equal(4, result);
        }

        [Fact]
        public void Calculator_Should_Subtract()
        {
            var result = _calculator.Subtract(5, 2);

            Assert.Equal(3, result);
        }
    }

    public class Calculator : ICalculator
    {
        public int Add(int a, int b)
        {
            return a + b;
        }

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

    public interface ICalculator
    {
        int Add(int a, int b);
        int Subtract(int a, int b);
    }
}

目标是模拟对象的部分,以便仅测试该对象的另一部分。您不会测试被模拟的内容,这没有意义。 - L-Four
1
你完全理解模拟的目的吗? - hackp0int
实际上,正确的目的是隔离那个单独的部分。 - hackp0int
你的实现案例很好地证明了Add()测试计算Add(2,3)而不是(2,2)的必要性。 - Lynn Crumbling
@L-Three 是的,我现在明白了。我不应该在这些测试中使用模拟。非常感谢。 - Stan
显示剩余4条评论
4个回答

7

目的是在没有计算器本身的情况下测试依赖于计算器的类。在您的测试中,您知道计算器不能是失败的原因,因为它返回了正确的答案。

通过隔离要测试的代码,您将能够测试真正的代码单元。并且可以准确地查看导致测试失败的原因。


现在我明白了。所以这个类不应该使用模拟,但是使用ICalculator的不同类应该在它的测试中使用计算器模拟。非常清楚。谢谢。 - Stan

6

不应该对模拟对象进行单元测试。假设您想要测试使用IService和IStorage的OrderProcessor对象。

为了“单元测试”OrderProcessor,您可以模拟IService和IStorage的行为,以便验证目标类按预期工作,而无需使用Web服务和数据库。

即:

class OrderProcessor{
 private IService service, IStorage storage;
 public OrderProcessor(IService service, IStorage storage){ // bla bla}

 public ProcessOrder(Order o){
   // do something

  // use the service
  var price = service.GetPrice(..);


  // store the result
  storage.StoreOrder(order);
 }
}

// test. Define mocks
var mockStorage = new Mock<IStorage>();
var mockService = new Mock<IService>();

// Setup test behaviour
mockStorage.Setup(m => m.GetPrice("X10").Returns(11);
mockStorage.Setup(m => m.GetPrice("X11").Returns(99);
...
var target = new OrderProcessor(mockService.Object, mockStorage.Object);

// ...
target.ProcessOrder(o);

// Verify the storing was called
mockStorage.Verify(m => m.StoreOrder(o), Times.Once());

// Verify the service was called X times
mockService .Verify(m => m.GetPrice(x), Times.Exactly(order.Items.Count));

5
在这种情况下,模拟没有意义——这个例子太简单了。模拟ICalculator并不能获得什么收益。
只有在你的实现非常复杂,你试图测试一个依赖于这个接口实现的东西时,你才会进行模拟。但在这种情况下,你正在测试一个模拟实现。测试模拟实现是毫无意义的。
例如,假设你的计算器实现实际上调用了一个 web 服务来执行计算,而你正试图测试一个从该服务获取计算结果的消费者。你的目标不是测试计算器,而是测试使用计算器的东西。让你的测试依赖于 web 服务正在运行是愚蠢的,并且可能导致你的测试出乎意料地失败。

3

Mock可以替代依赖。

例如:

public interface IAddModule
{
    int Add(int lhs, int rhs);
}

public class Calculator
{
    private readonly IAddModule _addModule;

    public Calculator(IAddModule addModule)
    {
        _addModule = addModule;
    }

    public int Add(int lhs, int rhs)
    {
        return _addModule.Add(lhs, rhs);
    }
}

Calculator类依赖于IAddModule。根据IAddModule的实现方式,它可能具有记录日志或非托管代码等副作用。为了隔离依赖关系,您可以使用Mock代替IAddModule来测试该类。

public class CalculatorTests
{
    private readonly Calculcator _calculator;

    public CalculatorTests()
    {
        var mock = new Mock<IAddModule>();
        mock.Setup(a => a.Add(2, 2)).Returns(4);
        _calculator = new Calculator(mock.Object);
    }

    [Fact]
    public void Given_2_And_2_Then_4_Is_Returned()
    {
        var result = _calculator.Add(2, 2);

        Assert.Equal(4, result);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接