我在C#中遇到了关于模拟和测试的相当大的昙花一现。我对这个问题的解决方案是不可取的。
我有三个课程一起执行某些功能。使用接口或显式声明任何虚拟方法没有任何意义,因为设计并不真正要求扩展或多态。任何使类可重用的努力只会使代码复杂化。
但是,因为我没有明确声明任何虚拟方法,所以我不能通过框架工作来模拟这些类并记录它们的调用。伪代码(使用Rhino.Mocks)看起来像这样。
var b = mockRepo.StrickMock<ClassB>();
var c = mockRepo.StrickMock<ClassC>();
var classUnderTest = new ClassUnderTest{ B = b, C = c};
Expect.Call( b.MethodA );
Expect.Call( c.MethodB );
mockRepo.ReplayAll();
classUnderTest.DoSomething();
mockRepo.VerifyAll();
按照目前的情况,我必须使b.MethodA和c.MethodB虚拟化才能实现。或者,我可以提取ClassB和ClassC的接口并模拟它们。但正如我之前所说,这只会使事情复杂化。我可以改变类的设计,使其更具可重用性,但这也会使问题变得复杂,而且重用的可能性很小。
我应该如何解决这个问题,并且仍然努力保持依赖关系和执行代码之间的简单性?我错过了一个选项吗?你更喜欢采取什么方法?
答案 0 :(得分:2)
接口是您的单元测试的接缝,可以轻松插入假货。我不知道它们会如何使事情复杂化(除了增加类型的数量)。
接口使调用者和被调用者之间的契约显式化。接口也提供了创建“名称系统”的机会。界面还可以更容易地看到不属于的成员(而不是可以吸引不相关方法的类)。
总之,付出的代价很小。
答案 1 :(得分:1)
通过确保代码以某种方式调用其依赖项的例程来测试您的功能并不理想。您的测试现在与您的方法的实现相关联,而不是您期望的行为。
我建议从问题开始:我尝试测试哪些行为?行为是一组前提条件,您要调用的操作(即方法调用),以及您要测试的一组后置条件。例如,如果您想测试将项目推入堆栈会使其计数增加1,那么您将编写一个名为的测试:
public void Push_OnAnyStack_IncreasesCountByOne()
更一般地说,您可以使用以下模板命名测试:
public void MethodToCall_WithGivenStateAndInputs_PerformsExpectedResult
我同意你的看法,为接口引入接口可能会过度工程化。因此,您应该查看代码结构,看看是否有一种方法可以通过设置前置条件(安排测试)来测试要测试的行为,通过调用特定方法来执行操作,并通过查询状态来确定后置条件。你正在测试的对象。这样您就不太关心如何实现方法,而更关心它执行的行为。如果您发现这是不可能的,则表明您需要重构您的设计以明确责任。它也可能表明接口实际上是正确的解决方案。
答案 2 :(得分:0)
我绝对会选择接口。
答案 3 :(得分:0)
SOLID原则中反复出现的主题之一是你应该依赖于抽象(开放封闭原则/依赖倒置原则)。无论您是使用接口还是使用虚拟方法的抽象类,模拟都是关于测试对象之间的关系或契约。
如果使用模拟框架,这些工具使用动态代理动态生成基于行为的验证实现。此策略意味着您正在使用可替换的抽象。
TypeMock等其他框架在获取JIT之前使用Profiler API拦截和重写IL。这是一个付费产品,有点慢,但它允许你拦截任何东西。
就个人而言,给出了选择。我将免费工具和设计用于抽象。代码将不那么灵活,但总体上更容易预测并且更容易测试。