我正在尝试编写单元测试并遇到一个问题,其中每个模拟对象依赖于另外3个对象。这看起来像这样。
var objC = new Mock<IObjectC>(IObjectG, IObjectH);
var objB = new Mock<IObjectB>(IObjectE, IObjectF);
var objA = new Mock<IObjectA>(IObjectB, IObjectC, IObjectD);
我做错了什么?
答案 0 :(得分:1)
我做错了什么?
您违反Law of Demeter并使用tight coupling组件创建系统。如果你坚持这个法律,并制定一个只调用以下成员的方法:
那么你就不会遇到复杂的测试设置问题。
之前:考虑与客户的钱包交谈的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);