使用深度嵌套的依赖项进行unit testing和dependency injection

假设遗留类和方法结构如下所示

public class Foo { public void Frob(int a, int b) { if (a == 1) { if (b == 1) { // does something } else { if (b == 2) { Bar bar = new Bar(); bar.Blah(a, b); } } } else { // does something } } } public class Bar { public void Blah(int a, int b) { if (a == 0) { // does something } else { if (b == 0) { // does something } else { Baz baz = new Baz(); baz.Save(a, b); } } } } public class Baz { public void Save(int a, int b) { // saves data to file, database, whatever } } 

然后假设管理层发出了一个模糊的任务,即对我们所做的每件新事物执行unit testing,无论是增加的function,修改的需求还是错误修复。

我可能是文字解释的坚持者,但我认为“unit testing”这个短语意味着什么。 例如,这并不意味着给定1和2的输入,只有当1和2保存到数据库时, Foo.Frob的unit testingFoo.Frob成功。 基于我所读到的内容,我认为它最终意味着基于1和2的输入, Frob引用了Bar.BlahBar.Blah是否做了应该做的事情不是我直接关注的问题。 如果我关注测试整个过程,我相信还有另一个术语,对吗? function测试? 场景测试? 随你。 如果我太僵硬,请纠正我!

坚持我刚才的严格解释,让我们假设我想尝试利用dependency injection,一个好处是我可以模拟我的类,以便我可以,例如,我不能将我的测试数据保存到数据库或文件或无论情况如何。 在这种情况下, Foo.Frob需要IBarIBar需要IBazIBaz可能需要一个数据库。 注入这些依赖关系在哪里? 进入Foo ? 或者Foo只需要IBar ,然后Foo负责创建IBaz的实例?

当您进入这样的嵌套结构时,您可以很快发现可能存在多个必需依赖项。 进行这种注射的首选方法是什么?

让我们从你的最后一个问题开始。 注入的依赖项在哪里:常见的方法是使用构造函数注入(如Fowler所述 )。 所以Foo在构造函数中注入了一个IBarIBarBar的具体实现又将IBaz注入其构造函数中。 最后, IBaz实现( Baz )注入了一个IDatabase (或其他)。 如果您使用诸如Castle Project之类的DI框架,您只需要求DI容器为您解析Foo实例。 然后,它将使用您配置的任何内容来确定您正在使用的IBar实现。 如果它确定您的IBar实施是Bar那么它将确定您正在使用的IBaz实现等。

这种方法给你的是,你可以独立地测试每个具体的实现,并检查它是否正确调用(模拟的)抽象。

为了评论你对过于严格等问题的担忧,我唯一可以说的是,在我看来,你正在选择正确的道路。 也就是说,当实施所有这些测试的实际成本变得明显时,管理层可能会感到意外。

希望这可以帮助。

您在post的第一部分中描述的那种测试(当您将所有部分组合在一起时)通常将其定义为集成测试。 作为解决方案的一个好习惯,您应该拥有一个unit testing项目和一个集成测试项目。 为了在代码中注入依赖性,第一个也是最重要的规则是使用接口进行编码。 假设这个,假设你的类包含一个接口作为成员,你想要注入/模拟它:你可以将它作为属性公开或使用类构造函数传递实现。 我更喜欢使用属性来公开依赖项,这样构造函数就不会变得太冗长。 我建议你使用NUnit或MBunit作为测试框架,Moq作为一个模拟框架(它的输出比Rhino mocks更清晰)这里是一些文档,里面有一些关于如何用Moq模拟的例子http://code.google.com / p / MOQ /维基/快速启动

希望能帮助到你

我不认为有一种“首选”方法可以解决这个问题,但是你的一个主要问题似乎是dependency injection,当你创建Foo ,你还需要创建可能没必要的Baz 。 一个简单的方法是让Bar不要直接依赖于IBaz而是依赖于LazyFunc ,允许你的IoC容器创建一个Bar实例而不立即创建Baz

例如:

 public interface IBar { void Blah(int a, int b); } public interface IBaz { void Save(int a, int b); } public class Foo { Func getBar; public Foo(Func getBar) { this.getBar = getBar; } public void Frob(int a, int b) { if (a == 1) { if (b == 1) { // does something } else { if (b == 2) { getBar().Blah(a, b); } } } else { // does something } } } public class Bar : IBar { Func getBaz; public Bar(Func getBaz) { this.getBaz = getBaz; } public void Blah(int a, int b) { if (a == 0) { // does something } else { if (b == 0) { // does something } else { getBaz().Save(a, b); } } } } public class Baz: IBaz { public void Save(int a, int b) { // saves data to file, database, whatever } } 

我说你的unit testing是正确的,它应该涵盖一个相当小的“单位”代码,尽管究竟有多少争论。 但是,如果它触及数据库,那几乎肯定不是unit testing – 我称之为集成测试。

当然,可能是“管理层”并不真正关心这些事情,并且对集成测试非常满意! 它们仍然是完全有效的测试,并且可能更容易添加,但不一定会导致更好的设计,如unit testing倾向于。

但是,是的,当你的IBAR被创建时,将你的IBaz注入你的IBar,并将你的IBar注入你的Foo。 这可以在构造函数或setter中完成。 构造函数(IMO)更好,因为它只导致创建有效对象。 你可以做的一个选项(称为穷人的DI)是重载构造函数,因此你可以传入一个IBar进行测试,并在代码中使用的无参数构造函数中创建一个Bar。 你失去了良好的设计效益,但值得考虑。

当你完成所有工作后,尝试使用像Ninject这样的IoC容器,它可以让你的生活更轻松。

(还要考虑像TypeMock或Moles这样的工具,它们可以在没有界面的情况下模拟东西 – 但请记住,这是作弊而你不会得到改进的设计,所以应该是最后的手段)。

当您使用深层嵌套的层次结构时遇到问题,这只意味着您没有注入足够的依赖项。

这里的问题是我们有Baz,看起来你需要将Baz传递给Foo,后者将它传递给最终调用方法的Bar。 这似乎很多工作,有点无用……

你应该做的是传递Baz作为Bar对象构造函数的参数。 然后应将Bar传递给Foo对象的构造函数。 Foo永远不应该触摸甚至不知道Baz的存在。 只有Bar关心Baz。 在测试Foo时,您将使用Bar接口的另一个实现。 这个实现可能只会记录Blah被调用的事实。 它不需要考虑Baz的存在。

你可能在想这样的事情:

 class Foo { Foo(Baz baz) { bar = new Bar(baz); } Frob() { bar.Blah() } } class Bar { Bar(Baz baz); void blah() { baz.biz(); } } 

你应该做这样的事情:

 class Foo { Foo(Bar bar); Frob() { bar.Blah() } } class Bar { Bar(Baz baz); void blah() { baz.biz(); } } 

如果你正确地做到了,每个对象应该只需要处理它直接与之交互的对象。

在您的实际代码中,您可以动态构建对象。 要做到这一点,你只需要传递BarFactory和BazFactory的实例来在需要时构造对象。 基本原则保持不变。

听起来有点像这里的斗争:

  1. 处理遗留代码
  2. 继续编写可维护的代码
  3. 测试你的代码(真的继续2)
  4. 而且,我认为,释放一些东西

管理层对“unit testing”的使用只能通过询问它们来确定,但这里是关于上述所有4个问题可能是个好主意的2c。

我认为测试Frob引用Bar.BlahBar.Blah做了它应该做的事情是很重要的。 虽然这些是不同的测试,但是为了发布无bug(或尽可能少的bug)软件,你真的需要进行unit testing( Frob调用Bar.Blah )以及集成测试( Bar.Blah做了它应该做的事情)。 如果你能对Bar.Blahunit testing,那将会很棒,但如果你不希望改变那么它可能不会太有用。

当然,你会在每次发现错误时添加unit testing,最好是在修复之前。 这样,您可以在修复之前确保测试中断,然后修复导致测试通过。

您不希望整天都在重构或重写代码库,因此您需要明智地处理依赖关系。 在您给出的示例中,在Foo内部,您可能最好将Bar提升为internal property并设置项目以使内部对您的测试项目可见(使用AssemblyInfo.cs中的InternalsVisibleTo属性)。 Bar的默认构造函数可以将该属性设置为new Bar() 。 您的测试可以将其设置为用于测试的Bar某个子类。 或者存根。 我认为这将减少你必须做出的改变量,以使这个东西可以测试。

当然,在对该类进行其他更改之前,您不需要对类进行任何重构。