假设遗留类和方法结构如下
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
}
}
然后假设管理层发布了一个模糊的任务,即对我们所做的每件新事物执行单元测试,无论是添加功能,修改需求还是错误修复。
我可能是文字解释的坚持者,但我认为“单元测试”这个词意味着什么。例如,它并不意味着给定1和2的输入,Foo.Frob
的单元测试只有在将1和2保存到数据库时才会成功。根据我所读到的内容,我认为它最终意味着基于1和2的输入,Frob
调用Bar.Blah
。 Bar.Blah
是否做了它应该做的事情不是我直接关注的问题。如果我关心测试整个过程,我相信还有另一个术语,对吗?功能测试?场景测试?随你。如果我太僵硬,请纠正我!
坚持我刚才的严格解释,让我们假设我想尝试利用依赖注入,一个好处是我可以模拟我的类,以便我可以,例如,不将我的测试数据保存到数据库或文件或任何情况下。在这种情况下,Foo.Frob
需要IBar
,IBar
需要IBaz
,IBaz
可能需要数据库。注入这些依赖项在哪里?进入Foo
?或Foo
仅需要IBar
,然后Foo
负责创建IBaz
的实例?
当你进入这样的嵌套结构时,你可以很快发现可能存在多个依赖关系。进行这种注射的首选方法或接受方法是什么?
答案 0 :(得分:7)
让我们从你的上一个问题开始。注入的依赖项在哪里:一种常见的方法是使用构造函数注入(如described by Fowler)。因此Foo
在构造函数中注入了IBar
。 IBar
,Bar
的具体实现又将IBaz
注入其构造函数中。最后,IBaz
实现(Baz
)注入了IDatabase
(或其他)。如果您使用诸如Castle Project之类的DI框架,您只需要求DI容器为您解析Foo
的实例。然后,它将使用您配置的任何内容来确定您正在使用的IBar
的实现。如果它确定您的IBar
实施是Bar
,那么它将确定您正在使用的IBaz
的哪个实施,等等。
这种方法给你的是,你可以单独测试每个具体的实现,并检查它是否正确调用(模拟的)抽象。
评论你对过于严格等问题的担忧,我唯一可以说的是,在我看来,你正在选择正确的道路。也就是说,当实施所有这些测试的实际成本变得明显时,管理层可能会感到意外。
希望这有帮助。
答案 1 :(得分:2)
我认为没有一种“首选”方法可以解决这个问题,但您的一个主要问题似乎是依赖注入,当您创建Foo
时,您还需要创建{{1这可能是不必要的。解决此问题的一个简单方法是Baz
不要直接依赖Bar
,而是依赖IBaz
或Lazy<IBaz>
,允许您的IoC容器创建{{1}的实例没有立即创建Func<IBaz>
。
例如:
Bar
答案 2 :(得分:2)
您在帖子的第一部分中描述的测试类型(当您将所有部分组合在一起时)通常将其定义为集成测试。作为解决方案的一个好习惯,您应该拥有一个单元测试项目和一个集成测试项目。为了在代码中注入依赖性,第一个也是最重要的规则是使用接口进行编码。假设这个,假设您的类包含一个接口作为成员,并且您想要注入/模拟它:您可以将其作为属性公开或使用类构造函数传递实现。 我更喜欢使用属性来公开依赖项,这样构造函数就不会变得太冗长。 我建议你使用NUnit或MBunit作为测试框架,使用Moq作为模拟框架(它的输出比Rhino mocks更清晰) 这里是一些文档,其中包含一些如何使用Moq http://code.google.com/p/moq/wiki/QuickStart
进行模拟的示例希望有所帮助
答案 3 :(得分:1)
我说你的单元测试是正确的,它应该涵盖一个相当小的“单位”代码,尽管究竟有多少争论。但是,如果它触及数据库,那几乎肯定不是单元测试 - 我称之为集成测试。
当然,可能是'管理'并不真正关心这些事情,并且对集成测试非常满意!它们仍然是完全有效的测试,并且可能更容易添加,但不一定会导致更好的设计,如单元测试倾向于。
但是,是的,当你的IBAR被创建时将它注入你的IBar,并将你的IBar注入你的Foo。这可以在构造函数或setter中完成。构造函数是(IMO)更好,因为它只导致创建有效对象。你可以做的一个选项(称为穷人的DI)是重载构造函数,因此你可以传入一个IBar进行测试,并在代码中使用的无参数构造函数中创建一个Bar。你失去了良好的设计效益,但值得考虑。
当您完成所有工作后,请尝试使用Ninject等IoC容器,这可能会让您的生活更轻松。
(还要考虑TypeMock或Moles之类的工具,这些工具可以在没有界面的情况下模拟事物 - 但要记住这是作弊而你不会得到改进的设计,所以应该是最后一个度假区)。
答案 4 :(得分:0)
当您使用深层嵌套的层次结构时遇到问题,这只意味着您没有注入足够的依赖项。
这里的问题是我们有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的实例来在需要时构造对象。基本原则保持不变。
答案 5 :(得分:0)
这听起来像是有点争吵:
管理层对“单元测试”的使用只能通过询问他们来确定,但这里是关于上述所有4个问题可能是个好主意的2c。
我认为测试Frob
调用Bar.Blah
和Bar.Blah
做了它应该做的事情是很重要的。虽然这些是不同的测试,但是为了发布无bug(或尽可能少的bug)软件,你真的需要进行单元测试(Frob
调用Bar.Blah
)作为集成测试(Bar.Blah
做了它应该做的事情)。如果您也可以对Bar.Blah
进行单元测试,那将会非常棒,但如果您不希望这一点进行更改,则可能不会太有用。
当然,你需要在每次发现错误时添加单元测试,最好是在修复之前。这样,您可以在修复之前确保测试中断,然后修复导致测试通过。
您不希望整天都在重构或重写代码库,因此您需要明智地了解如何处理依赖项。在您给出的示例中,在Foo
内,您可能最好将Bar
推广到internal property
并设置项目以使内部对您的测试项目可见(使用InternalsVisibleTo
AssemblyInfo.cs中的}属性)。 Bar
的默认构造函数可以将属性设置为new Bar()
。您的测试可以将其设置为用于测试的Bar
的某个子类。或者存根。我认为这将减少你必须做出的改变,以使这个东西可以测试。
当然,在对该类进行其他更改之前,您不需要对类进行任何重构。