C# 具有深度嵌套依赖项的单元测试和依赖项注入

C# 具有深度嵌套依赖项的单元测试和依赖项注入,c#,unit-testing,dependency-injection,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 ==

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

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
    }
}
然后假设管理层发布了一项模糊的任务,即对我们所做的每一件新事情执行单元测试,无论是添加的特性、修改的需求还是bug修复

我可能是一个坚持字面解释的人,但我认为“单元测试”这个短语意味着什么。例如,这并不意味着给定1和2的输入,只有将1和2保存到数据库中,
Foo.Frob
的单元测试才会成功。根据我所读到的,我相信它最终意味着基于1和2的输入,
Frob
调用
Bar.Blah
Bar.Blah
是否做了它应该做的事情并不是我当前关心的问题。如果我关心整个过程的测试,我相信还有另外一个术语,对吗?功能测试?场景测试?无论什么如果我太刻板,请纠正我

目前坚持我的严格解释,让我们假设我想尝试利用依赖注入,一个好处是我可以模仿我的类,这样我就可以,例如,不将测试数据持久化到数据库或文件或任何情况。在这种情况下,
Foo.Frob
需要
IBar
IBar
需要
IBaz
IBaz
可能需要一个数据库。这些依赖关系将注入到哪里?进入
Foo
?或者
Foo
仅仅需要
IBar
,然后
Foo
负责创建
IBaz
的实例


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

让我们从您的最后一个问题开始。注入依赖项的位置:常用的方法是使用构造函数注入(as)。因此
Foo
在构造函数中注入了
IBar
IBar
Bar
的具体实现将
IBaz
注入其构造函数。最后,
IBaz
实现(
Baz
)注入了一个
IDatabase
(或任何东西)。如果您使用DI框架,例如,您只需请求DI容器为您解析
Foo
的实例。然后,它将使用您配置的任何内容来确定您使用的是哪个
IBar
实现。如果它确定您的
IBar
实现是
Bar
,那么它将确定您正在使用的
IBaz
实现,等等

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

要评论你对过于僵化等问题的担忧,我唯一能说的是,在我看来,你选择了正确的道路。也就是说,当实施所有这些测试的实际成本对管理层来说变得显而易见时,他们可能会感到惊讶


希望这能有所帮助。

我不认为有一种“首选”方法可以解决这个问题,但您主要关心的问题之一似乎是,使用依赖注入,当您创建
Foo
时,您还需要创建
Baz
,这可能是不必要的。解决这个问题的一个简单方法是,
Bar
不直接依赖于
IBaz
,而是依赖于
Lazy
Func
,允许您的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<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
    }
}
公共接口IBar
{
无效废话(内部a、内部b);
}
公共接口IBaz
{
无效保存(内部a、内部b);
}
公开课Foo
{
Func-getBar;
公共Foo(Func getBar)
{
this.getBar=getBar;
}
公共无效Frob(内部a、内部b)
{
如果(a==1)
{
如果(b==1)
{
//做点什么
}
其他的
{
如果(b==2)
{                        
getBar().Blah(a,b);
}
}
}
其他的
{
//做点什么
}
}
}
公共类酒吧:IBar
{
Func getBaz;
公共酒吧(Func getBaz)
{
this.getBaz=getBaz;
}
公共空间空洞空洞(内部a、内部b)
{
如果(a==0)
{
//做点什么
}
其他的
{
如果(b==0)
{
//做点什么
}
其他的
{
getBaz().Save(a,b);
}
}
}
}
公共类Baz:IBaz
{
公共作废保存(int a、int b)
{
//将数据保存到文件、数据库等
}
}

关于单元测试,我认为你是对的,它应该涵盖相当小的“单元”代码,尽管到底有多少值得商榷。然而,如果它涉及到数据库,那几乎肯定不是一个单元测试——我称之为集成测试

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

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

当你解决了所有这些问题后,试一下IoC容器,比如,它可能会
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();
     }
}