嵌套的必需对象

Nested required objects

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

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

我做错了什么?

这可能表明您正在测试的代码的设计存在缺陷。这还不错——这是一件好事。可能不是 - 问题可能出在您试图通过测试完成什么。

如果您要模拟 IObjectA,为什么需要模拟其依赖项?如果您正在测试的 class 依赖于 IObjectA,那么 IObjectA 的实现是否有自己的依赖性是否重要?依赖抽象的一个好处是 class 不必关心其依赖项的实现细节。换句话说,你的 class 应该关心的是 IObjectA 做了什么。它不应该知道或关心 IObjectA 是否有依赖性,更不用说它们做了什么。

如果你的 class "knows" 那个 IObjectA 代表一个 class 有自己的依赖关系,那么它实际上依赖的不仅仅是接口。该问题可能表明您应该重构 class 以便它 依赖于接口,而不是实现该接口的 class 的依赖项。


如果 IObjectA 具有需要 return 实现其他接口的属性或方法,那么您可以为这些接口创建模拟,然后将 IObjectA 的模拟配置为 return 那些嘲笑。

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

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

现在,回到你的问题,我想知道你真正描述的是否是你想调用一个模拟 return 另一个模拟的情况。这可能发生在诸如想要 return 来自模拟工厂的模拟策略等场景中。为此,您必须将工厂设置为 return 策略。像这样:

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 的方法将 return 模拟策略。从那里,您可以对策略做任何您需要的事情,例如验证对它的调用:

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

What am I doing wrong?

您违反了 Law of Demeter and creating system with the tight coupling 的组件。如果您坚持这条法则并创建一个仅调用以下成员的方法:

  1. 对象本身。
  2. 方法的参数。
  3. 在方法中创建的任何对象。
  4. 对象的任何直接 properties/fields。

那么你就不会有复杂的测试设置问题了。

之前: 考虑与客户钱包通信的 ATM class:

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

之后: 现在考虑仅与其参数成员对话的方法(得墨忒耳法则 #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();
}    

请注意,不再涉及真正的 classes。我们用抽象的 IClient 依赖替换了 Person 依赖。如果在 Person 实现中出现问题,不会影响 ATM 测试。


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

[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 class 的实现。例如。您可以从钱包切换到信用卡,或者如果钱包是空的,请检查信用卡。