使用深层嵌套依赖项进行单元测试和依赖注入

时间:2010-11-10 17:08:46

标签: c# 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
    }
}

然后假设管理层发布了一个模糊的任务,即对我们所做的每件新事物执行单元测试,无论是添加功能,修改需求还是错误修复。

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

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

当你进入这样的嵌套结构时,你可以很快发现可能存在多个依赖关系。进行这种注射的首选方法或接受方法是什么?

6 个答案:

答案 0 :(得分:7)

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

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

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

希望这有帮助。

答案 1 :(得分:2)

我认为没有一种“首选”方法可以解决这个问题,但您的一个主要问题似乎是依赖注入,当您创建Foo时,您还需要创建{{1这可能是不必要的。解决此问题的一个简单方法是Baz不要直接依赖Bar,而是依赖IBazLazy<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容器,这可能会让您的生活更轻松。

(还要考虑TypeMockMoles之类的工具,这些工具可以在没有界面的情况下模拟事物 - 但要记住这是作弊而你不会得到改进的设计,所以应该是最后一个度假区)。

答案 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)

这听起来像是有点争吵:

  1. 处理遗留代码
  2. 继续编写可维护代码
  3. 测试你的代码(真的继续2)
  4. 而且,我认为,发布了一些东西
  5. 管理层对“单元测试”的使用只能通过询问他们来确定,但这里是关于上述所有4个问题可能是个好主意的2c。

    我认为测试Frob调用Bar.BlahBar.Blah做了它应该做的事情是很重要的。虽然这些是不同的测试,但是为了发布无bug(或尽可能少的bug)软件,你真的需要进行单元测试(Frob调用Bar.Blah)作为集成测试(Bar.Blah做了它应该做的事情)。如果您也可以对Bar.Blah进行单元测试,那将会非常棒,但如果您不希望这一点进行更改,则可能不会太有用。

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

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

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

相关问题