模拟像Calculator这样的类的目的是什么?

本文关键字:是什么 Calculator 模拟 | 更新日期: 2023-09-27 18:02:14

我已经使用TDD有一段时间了,但现在我正在寻找嘲弄框架,我没有得到一些东西。对于有经验的人来说,这个问题可能听起来很愚蠢,但我就是不明白。我使用的库是Moq + xUnit。

问题

如果我明确地说2 + 2将在这行mock.Setup(x => x.Add(2, 2)).Returns(4);上返回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);
    }
}

模拟像Calculator这样的类的目的是什么?

目的是能够根据计算器测试类,而不需要计算器是自己的。在你的测试中,你知道计算器不可能是失败的原因,因为它返回了正确的答案。

通过隔离被测代码,您将能够测试真正的代码单元。同时也要看看到底是什么导致了测试失败。

不应该对模拟进行单元测试。假设你想测试一个使用IService和storage的OrderProcessor对象。

要"单元测试"OrderProcessor,您可以模拟IService和storage的行为,以便您可以验证您的目标类是否按预期工作,而无需使用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));

在这种情况下,嘲弄是没有意义的——示例太简单了。你不会因为嘲笑ICalculator而得到任何东西。

当你有一个复杂的实现,并且你试图测试一些依赖于接口的实现的东西时,你可以模拟。在本例中,您没有这样做,而是测试一个模拟实现。测试模拟实现是没有意义的。

例如,假设您的计算器实现实际上调用了一个web服务来执行计算,并且您正在尝试测试一些从服务中消耗计算的东西。你的目标不是测试计算器——你的目标是测试使用计算器的东西。让您的测试依赖于正在启动和运行的web服务是愚蠢的,并且可能导致您的测试意外失败。

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