嵌套的必需对象

时间:2017-05-10 17:37:37

标签: c# unit-testing moq

我正在尝试编写单元测试并遇到一个问题,其中每个模拟对象依赖于另外3个对象。这看起来像这样。

var objC = new Mock<IObjectC>(IObjectG, IObjectH);
var objB = new Mock<IObjectB>(IObjectE, IObjectF);
var objA = new Mock<IObjectA>(IObjectB, IObjectC, IObjectD);

我做错了什么?

3 个答案:

答案 0 :(得分:1)

  

我做错了什么?

您违反Law of Demeter并使用tight coupling组件创建系统。如果你坚持这个法律,并制定一个只调用以下成员的方法:

  1. 对象本身。
  2. 方法的论据。
  3. 在方法中创建的任何对象。
  4. 对象的任何直接属性/字段。
  5. 那么你就不会遇到复杂的测试设置问题。

    之前:考虑与客户的钱包交谈的ATM课程:

    public void ProcessPayment(Person client, decimal amount)
    {
        var wallet = client.Wallet;
        if (wallet.TotalAmount() < amount)
           throw new BlahBlahException();
    
        wallet.Remove(amount);
    }
    

    具有复杂的

    设置
    [Test]
    public void AtmShouldChargeClientWhenItHasEnoughMoney()
    {
        var walletMock = new Mock<IWallet>();
        walletMock.Setup(w => w.GetTotalAmount()).Returns(15);
        var personMock = new Mock<Person>(walletMock.Object);
        var atm = new Atm();
    
        atm.ProcessPayment(personMock.Object, 10);
    
        walletMock.Verify(w => w.Remove(10), Times.Once);
    }
    

    AFTER:现在考虑一下只讨论其论点成员的方法(Demeter法则#2)

    public void ProcessPayment(IClient client, decimal amount)
    {
        if (!client.TryCharge(amount))
           throw new BlahBlahException();
    }
    

    不仅代码变得简单易读,而且简化了测试设置:

    [Test]
    public void AtmShouldChargeClientWhenItHasEnoughMoney()
    {
        var clientMock = new Mock<IClient>();
        clientMock.Setup(c => c.TryCharge(10)).Returns(true);
        var atm = new Atm();
    
        atm.ProcessPayment(clientMock.Object, 10);
        clientMock.VerifyAll();
    }    
    

    请注意,不再有真正的类调用。我们用抽象IClient依赖替换了Person依赖。如果在Person实现中某些内容被破坏,它将不会影响ATM测试。

    当然,您应该对Person类进行单独测试,以检查它是否与钱包正确交互:

    [Test]
    public void PersonShouldNotBeChargedWhenThereIsNotEnoughMoneyInWallet()
    {
        var walletMock = new Mock<IWallet>(MockBehavior.Strict);
        walletMock.Setup(w => w.GetTotalAmount()).Returns(5);
        var person = new Person(walletMock.Object);
    
        person.TryCharge(10).Should().BeFalse();
        walletMock.VerifyAll();
    }
    
    [Test]
    public void PersonShouldBeChargedWhenThereIsEnoughMoneyInWallet()
    {
        var walletMock = new Mock<IWallet>(MockBehavior.Strict);
        walletMock.Setup(w => w.GetTotalAmount()).Returns(15);
        walletMock.Setup(w => w.Remove(10));
        var person = new Person(walletMock.Object);
    
        person.TryCharge(10).Should().BeTrue();
        walletMock.VerifyAll();
    }
    

    好处 - 您可以在不破坏ATM功能和测试的情况下更改Person类的实现。例如。你可以从钱包换成信用卡,或者如果钱包是空的,也可以查看信用卡。

答案 1 :(得分:0)

这可能表明您正在测试的代码设计存在缺陷。那不错 - 这是件好事。它可能不是 - 问题可能是你试图通过测试完成的。

如果你在嘲笑IObjectA,你为什么需要嘲笑它的依赖?如果您正在测试的类取决于IObjectA,那么IObjectA的实现是否有自己的依赖关系是否重要?依赖于抽象的一个好处是类不必关心其依赖项的实现细节。换句话说,你所有的课程都应该关注的是IObjectA的作用。它不应该知道或关心IObjectA是否有依赖关系,更不用说他们做什么了。

如果你的班级“知道”IObjectA代表一个有自己依赖关系的类,那么它实际上取决于接口以外的东西。问题可能表明你应该重构你的类,使它 only 依赖于接口,而不是实现接口的类的依赖。

如果IObjectA具有需要返回其他接口实现的属性或方法,则可以为这些接口创建模拟,然后配置模拟IObjectA以返回这些模拟。

答案 2 :(得分:0)

模拟类型的目的是让我们可以编写测试而无需处理复杂的依赖图,或者不必担心与任何外部进程进行通信,因此我们可以专注于编写快速,确定性的单元测试。这意味着当您创建模拟时,该模拟的内部表示与您的测试无关;它只是一个代理对象,它取代了代码在生产中使用的实际实现。

也就是说,我们仍然需要能够配置这些模拟来展示我们想要的行为 - 例如,返回值或抛出异常 - 这就是为什么我们可以通过调用Setup()来配置它们的设置。

现在,回到你的问题,我想知道你真正描述的是你想要调用mock来返回另一个mock的情况。这种情况可能发生在诸如想要从模拟工厂返回模拟策略的情况中。要做到这一点,您必须设置工厂以返回策略。像这样:

var factoryMock = new Mock<IFactory>();
var strategyMock = new Mock<IStrategy>();
var type = typeof(FakeConcreteStrategy);

factoryMock.Setup(x => x.Create(type)).Returns(strategyMock.Object);

通过上述操作,调用工厂Create方法的类型为FakeConcreteStrategy将返回模拟策略。从那里,您可以根据策略做任何您需要的事情,例如验证对它的调用:

strategyMock.Verify(x => x.DoWork(), Times.Once);