代码之家  ›  专栏  ›  技术社区  ›  Anthony Pegram

具有深度嵌套依赖关系的单元测试和依赖注入

  •  7
  • Anthony Pegram  · 技术社区  · 14 年前

    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
        }
    }
    

    然后假设管理层发布了一个模糊不清的命令,要求对我们所做的每一件新事情执行单元测试,无论是添加的特性、修改的需求,还是错误修复。

    Foo.Frob 仅当1和2保存到数据库时才应成功。根据我所读到的,我相信它最终意味着基于1和2的输入, Frob 调用 Bar.Blah . 是否 做了该做的事不是我的当务之急。如果我想测试整个过程,我相信还有另一个术语,对吧?功能测试?情景测试?无论什么。如果我太刻板,请纠正我!

    暂时坚持我僵硬的解释,假设我想尝试使用依赖注入,一个好处是我可以模拟我的类,这样我就可以,例如, 将我的测试数据持久化到数据库或文件或任何情况。在这种情况下, 需要 IBar , 国际银行同业拆借利率 需要 IBaz , 伊巴斯 Foo ? 或者 只是需要 ,然后 负责创建的实例 ?

    当您进入这样的嵌套结构时,您可以很快看到可能需要多个依赖项。进行这种注射的首选或可接受的方法是什么?

    6 回复  |  直到 14 年前
        1
  •  7
  •   Klaus Byskov Pedersen    14 年前

    让我们从你的最后一个问题开始。在何处注入依赖项:一种常见的方法是使用构造函数注入(如 described by Fowler ). 所以 Foo IBar 在构造器中。具体实施 国际银行同业拆借利率 , Bar 依次有一个 IBaz 伊巴斯 实施( Baz IDatabase (或其他)注射。如果您使用诸如 Castle Project ,您只需请求DI容器解析 为你。然后它将使用您配置的任何内容来确定 国际银行同业拆借利率 国际银行同业拆借利率 酒吧 伊巴斯 你正在使用等等。

    我只能说,在我看来,你选择的道路是正确的。也就是说,当实施所有这些测试的实际成本对管理层来说变得显而易见时,管理层可能会大吃一惊。

        2
  •  2
  •   Giorgio Minardi    14 年前

    您在文章的第一部分描述的测试(当您尝试所有部分时)通常被定义为集成测试。作为解决方案中的一个良好实践,您应该有一个单元测试项目和一个集成测试项目。为了在代码中注入依赖项,第一个也是最重要的规则是使用接口编写代码。假设这样,假设您的类包含一个作为成员的接口,并且您希望对其进行注入/模拟:您可以将其作为属性公开,也可以使用类构造函数传递实现。 我建议您使用NUnit或MBunit作为测试框架,使用Moq作为模拟框架(它的输出比Rhino mock更清晰) 下面的文档中有一些关于如何使用Moq模拟的示例 http://code.google.com/p/moq/wiki/QuickStart

    希望有帮助

        3
  •  2
  •   Mark Heath    14 年前

    Foo ,您还需要创建 Baz Bar 不直接依靠 IBaz Lazy<IBaz> 或者 Func<IBaz> ,允许IoC容器创建 酒吧 巴兹 .

    例如:

    public interface IBar
    {
        void Blah(int a, int b);
    }
    
    public interface IBaz
    {
        void Save(int a, int b);
    }
    
    public class Foo
    {
        Func<IBar> getBar;
        public Foo(Func<IBar> 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<IBaz> getBaz;
    
        public Bar(Func<IBaz> 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
        }
    }
    
        4
  •  1
  •   Grant Crofton    14 年前

    当然,这可能是因为“管理层”并不真正关心这些事情,他们对集成测试非常满意!它们仍然是完全有效的测试,而且对您来说可能更容易添加,尽管不一定会导致更好的设计,比如单元测试。

    当您完成了所有这些工作后,可以尝试一个IoC容器,例如 Ninject ,这可能会让你的生活更轻松。

    (同时考虑以下工具 TypeMock Moles ,它可以在没有界面的情况下模拟事物-但请记住这是欺骗,并且您不会得到改进的设计,因此应该是最后的办法)。

        5
  •  0
  •   Winston Ewert    14 年前

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

    您应该做的是将Baz作为Bar对象构造函数的参数传递。然后应该将Bar传递给Foo对象的构造函数。FoO永远不应该接触甚至知道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();
         }
    }
    

    如果您做得正确,那么每个对象只需要处理它直接与之交互的对象。

        6
  •  0
  •   hackerhasid    14 年前

    听起来有点挣扎:

    1. 处理遗留代码
    2. 测试代码(真的是2的继续)
    3. 而且,我想,释放一些东西

    我认为这两个测试都很重要 Frob 调用 Bar.Blah 还有那个 做了该做的事。虽然这些测试是不同的,但是为了发布无bug(或者尽可能少的bug)软件,您确实需要进行单元测试( 青蛙 酒吧,废话 )以及集成测试( 酒吧,废话 做了该做的事)。如果你能进行单元测试那就太好了 但如果你不希望这种情况发生变化,那么它可能就不会太有用了。

    当然,在每次发现bug时,最好先添加单元测试,最好是在修复之前。这样你就可以确保测试在修复前中断,然后修复会使测试通过。

    Foo 你最好去宣传一下 Bar 给一个 internal property 并设置项目以使内部内容对测试项目可见(使用 InternalsVisibleTo AssemblyInfo.cs中的属性)。的默认构造函数 可以将属性设置为 new Bar() 酒吧

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