简体   繁体   English

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

[英]Unit testing and dependency injection with deeply nested dependencies

Assume a legacy class and method structure like below 假设遗留类和方法结构如下所示

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

And then assume management issues a nebulous mandate to perform unit testing for every new thing we do, be it an added feature, modified requirement, or bug fix. 然后假设管理层发布了一个模糊的任务,即对我们所做的每件新事物执行单元测试,无论是增加的功能,修改的需求还是错误修复。

I may be a stickler for literal interpretation, but I think the phrase "unit testing" means something. 我可能是文字解释的坚持者,但我认为“单元测试”这个短语意味着什么。 It does not mean, for example, that given inputs of 1 and 2 that the unit test of Foo.Frob should succeed only if 1 and 2 are saved to a database. 例如,这并不意味着给定1和2的输入,只有当1和2保存到数据库时, Foo.Frob的单元测试Foo.Frob成功。 Based on what I've read, I believe it ultimately means based on inputs of 1 and 2, Frob invoked Bar.Blah . 基于我所读到的内容,我相信它最终意味着基于1和2的输入, Frob调用了Bar.Blah Whether or not Bar.Blah did what it is supposed to do is not my immediate concern. Bar.Blah是否做了应该做的事情并非我直接关注的问题。 If I'm concerned with testing the entire process, I believe there's another term for that, right? 如果我关心测试整个过程,我相信还有另一个术语,对吗? Functional testing? 功能测试? Scenario testing? 场景测试? Whatever. 随你。 Correct me if I'm being too rigid, please! 如果我太僵硬,请纠正我,拜托!

Sticking with my rigid interpretation for the moment, let's assume I want to try to utilize dependency injection, with one benefit being that I can mock away my classes so that I can, for example, not persist my test data to a database or file or whatever the case may be. 坚持我刚才的严格解释,让我们假设我想尝试利用依赖注入,一个好处是我可以模拟我的类,以便我可以,例如,我不能将我的测试数据保存到数据库或文件或无论情况如何。 In this case, Foo.Frob needs IBar , IBar needs IBaz , IBaz may need a database. 在这种情况下, Foo.Frob需要IBarIBar需要IBazIBaz可能需要一个数据库。 Where are these dependencies to be injected? 注入这些依赖项在哪里? Into Foo ? 进入Foo Or does Foo merely need IBar , and then Foo is responsible for creating an instance of IBaz ? 或者Foo只需要IBar ,然后Foo负责创建IBaz的实例?

When you get into a nested structure such as this, you can quickly see there could be multiple dependencies necessary. 当您进入这样的嵌套结构时,您可以快速看到可能存在多个必需依赖项。 What is the preferred or accepted method of performing such injection? 进行这种注射的首选方法是什么?

Let us start with your last question. 让我们从你的上一个问题开始。 Where are the dependencies injected: A common approach is to use constructor injection (as described by Fowler ). 注入的依赖项在哪里:一种常见的方法是使用构造函数注入(如Fowler所述 )。 So Foo is injected with an IBar in the constructor. 所以Foo在构造函数中注入了一个IBar The concrete implementation of IBar , Bar in turn has an IBaz injected into its constructor. IBarBar的具体实现又将IBaz注入其构造函数中。 And finally the IBaz implementation ( Baz ) has an IDatabase (or whatever) injected. 最后, IBaz实现( Baz )注入了一个IDatabase (或其他)。 If you use a DI framework such as Castle Project , you would simply ask the DI container to resolve an instance of Foo for you. 如果您使用诸如Castle Project之类的DI框架,您只需要求DI容器为您解析Foo实例。 It will then use whatever you have configured to determine which implementation of IBar you are using. 然后,它将使用您配置的任何内容来确定您正在使用的IBar实现。 If it determines that your implementation of IBar is Bar it will then determine which implementation of IBaz you are using, etc. 如果它确定您的IBar实施是Bar那么它将确定您正在使用的IBaz实现等。

What this approach gives you, is that you can test each of the concrete implementations in isolation, and just check that it invokes the (mocked) abstraction correctly. 这种方法给你的是,你可以单独测试每个具体的实现,并检查它是否正确调用(模拟)抽象。

To comment on your concerns about being too rigid etc, the only thing I can say is that in my opinion you are choosing the right path. 为了评论你对过于严格等问题的担忧,我唯一可以说的是,在我看来,你正在选择正确的道路。 That said, management might be in for a surprise when the actual cost of implementing all those tests becomes apparent to them. 也就是说,当实施所有这些测试的实际成本变得明显时,管理层可能会感到意外。

Hope this helps. 希望这可以帮助。

I don't think there is one "preferred" method for addressing this, but one of your main concerns seems to be that with dependency injection, when you create Foo , you need to also create Baz which might be unnecessary. 我不认为有一种“首选”方法可以解决这个问题,但是您的一个主要问题似乎是依赖注入,当您创建Foo ,您还需要创建可能不必要的Baz One simple way around this is for Bar not to depend directly on IBaz but on a Lazy<IBaz> or a Func<IBaz> , allowing your IoC container to create an instance of Bar without immediately creating Baz . 一个简单的方法是让Bar不要直接依赖于IBaz而是依赖于Lazy<IBaz>Func<IBaz> ,允许你的IoC容器创建一个Bar实例而不立即创建Baz

For example: 例如:

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

the kind of test you described in the first part of your post (when you try all the parts together) it is usually defined as integration test. 您在帖子的第一部分中描述的那种测试(当您将所有部分组合在一起时)通常将其定义为集成测试。 As a good practice in your solution you should have either a unit test project and an integration test project. 作为解决方案的一个好习惯,您应该拥有一个单元测试项目和一个集成测试项目。 In order to inject dependecies in your code the first and most important rule is to code using interfaces. 为了在代码中注入依赖性,第一个也是最重要的规则是使用接口进行编码。 Assumed this, let's say your class contains an interface as a member and you want to inject/mock it: you can either expose it as a property or pass the implementation using the class constructor. 假设这个,假设您的类包含一个接口作为成员,并且您想要注入/模拟它:您可以将其作为属性公开或使用类构造函数传递实现。 I prefer to use properties to expose dependencies, this way the constructor don't become too verbose. 我更喜欢使用属性来公开依赖项,这样构造函数就不会变得太冗长。 I suggest you to use NUnit or MBunit as a testing framework and Moq as a mocking framework (more clear in it's outputs than Rhino mocks) Here's the documentation with a some examples on how to mock with Moq http://code.google.com/p/moq/wiki/QuickStart 我建议你使用NUnit或MBunit作为测试框架,Moq作为一个模拟框架(它的输出比Rhino mocks更清晰)这里有一些关于如何用Moq模拟的文档http://code.google.com / p / MOQ /维基/快速启动

Hope it helps 希望能帮助到你

I'd say you're right about unit testing, it should cover a fairly small 'unit' of code, although exactly how much is up for debate. 我说你的单元测试是正确的,它应该涵盖一个相当小的“单位”代码,尽管究竟有多少争论。 However, if it touches the database, that's almost certainly not a unit test - I'd call that an integration test. 但是,如果它触及数据库,那几乎肯定不是单元测试 - 我称之为集成测试。

Of course, it could be that 'management' don't really care about such things and would be quite happy with integration tests! 当然,可能是“管理层”并不真正关心这些事情,并且对集成测试非常满意! They're still perfectly valid tests, and probably easier for you to add in, although don't necessarily lead to better design like unit tests tend to. 它们仍然是完全有效的测试,并且可能更容易添加,但不一定导致更好的设计,如单元测试倾向于。

But yes, inject your IBaz into your IBar when that gets created, and inject your IBar into your Foo. 但是,是的,当你的IBAR被创建时,将你的IBaz注入你的IBar,并将你的IBar注入你的Foo。 This can be done in the constructor or a setter. 这可以在构造函数或setter中完成。 Constructor is (IMO) better as it leads to only valid objects being created. 构造函数是(IMO)更好,因为它只导致创建有效对象。 One option you can do (known as poor man's DI) is to overload the constructor, so you can pass in an IBar for testing, and create a Bar in the parameterless constructor used in code. 你可以做的一个选项(称为穷人的DI)是重载构造函数,因此你可以传入一个IBar进行测试,并在代码中使用的无参数构造函数中创建一个Bar。 You lose the good design benefits, but worth considering. 你失去了良好的设计效益,但值得考虑。

When you've worked all that out, try an IoC container such as Ninject , which may make your life easier. 当你完成所有这些工作后,试试一下像Ninject这样的IoC容器,它可以让你的生活更轻松。

(Also consider tools such as TypeMock or Moles , which can mock things without an interface - but bear in mind that's cheating and you won't get an improved design, so should be a last resort). (还要考虑TypeMockMoles之类的工具,它们可以在没有界面的情况下模拟事物 - 但请记住,作弊并且你不会得到改进的设计,所以应该是最后的手段)。

When you have trouble with a deeply nested hierarchy it just means you aren't injecting enough dependencies. 当您使用深层嵌套的层次结构时遇到问题,这只意味着您没有注入足够的依赖项。

The issue here is that we have Baz and it looks like you need to pass Baz to Foo who passes it to Bar who finally calls a method on it. 这里的问题是我们有Baz,看起来你需要将Baz传递给Foo,后者将它传递给Bar,后者最终调用了一个方法。 This seems like a lot of work and kinda useless... 这似乎很多工作,有点无用......

What you should do is pass Baz as the parameter of the Bar object constructor. 你应该做的是传递Baz作为Bar对象构造函数的参数。 Bar should then be passed to the constructor of the Foo object. 然后应将Bar传递给Foo对象的构造函数。 Foo should never touch or even know about the existence of Baz. Foo永远不应该触摸甚至不知道Baz的存在。 Only Bar cares about Baz. 只有Bar关心Baz。 When testing Foo, you would use another implementation of the Bar interface. 在测试Foo时,您将使用Bar接口的另一个实现。 This implementation probably does nothing but record that the fact that Blah was called. 这个实现可能只会记录Blah被调用的事实。 It does not need to consider the existence of Baz. 它不需要考虑Baz的存在。

You are probably thinking something like this: 你可能在想这样的事情:

class Foo
{
    Foo(Baz baz)
    {
         bar = new Bar(baz);
    }

    Frob()
    {
         bar.Blah()
    }
}

class Bar
{
     Bar(Baz baz);

     void blah()
     {
          baz.biz();
     }
}

You should do something like this: 你应该做这样的事情:

class Foo
{
    Foo(Bar bar);

    Frob()
    {
         bar.Blah()
    }
}

class Bar
{
     Bar(Baz baz);

     void blah()
     {
          baz.biz();
     }
}

If you do it correctly, each object should only need to deal with the objects it directly interacts with. 如果你正确地执行它,每个对象应该只需要处理它直接与之交互的对象。

In your actual code, you construct the objects on the fly. 在您的实际代码中,您可以动态构建对象。 To do that you'll just need to pass instances of BarFactory and BazFactory to do construct the objects when required. 要做到这一点,你只需要传递BarFactory和BazFactory的实例来在需要时构造对象。 The basic principle remains the same. 基本原则保持不变。

Sounds like there's a bit of a struggle here to: 听起来像这里有点斗争:

  1. deal with legacy code 处理遗留代码
  2. continue to write maintainable code 继续编写可维护的代码
  3. test your code (continuation of 2 really) 测试你的代码(真的继续2)
  4. and also, I assume, release something 而且,我认为,释放一些东西

Management's use of 'unit tests' can only by determined by asking them, but here's my 2c on what might be a good idea here in regards to all 4 issues above. 管理层对“单元测试”的使用只能通过询问他们来确定,但这里是关于上述所有4个问题可能是个好主意的2c。

I think it's important to both test that Frob invoked Bar.Blah and that Bar.Blah did what it's supposed to do. 我认为测试Frob引用Bar.BlahBar.Blah做了它应该做的事情是很重要的。 Granted these are different tests, but in order to release bug-free (or as-few-bugs-as-possible) software, you really need to have unit tests ( Frob invoked Bar.Blah ) as well as integration tests ( Bar.Blah did what it's supposed to do). 虽然这些是不同的测试,但是为了发布无bug(或尽可能少的bug)软件,你真的需要进行单元测试( Frob调用Bar.Blah )以及集成测试( Bar.Blah做了它应该做的事情)。 It would be great if you could unit test Bar.Blah too but if you don't expect that to change then it might not be too useful. 如果你能对Bar.Blah单元测试Bar.Blah但是如果你不希望改变它那么它可能不会太有用。

Certainly going forward you'll want to add unit tests every time you find a bug, preferably before fixing. 当然,你会在每次发现错误时添加单元测试,最好是在修复之前。 This way you can ensure the test breaks before fixing and then the fix causes the test to pass. 这样,您可以确保在修复之前测试中断,然后修复导致测试通过。

You don't want to spend all day refactoring or rewriting your code base so you need to be judicious in how you go about dealing with dependencies. 您不希望整天重构或重写代码库,因此您需要明智地处理依赖关系。 In the example you gave, inside Foo you might be best off promoting Bar to an internal property and setting up the project to make internals visible to your test project (using the InternalsVisibleTo attribute in AssemblyInfo.cs). 在您给出的示例中,在Foo内部,您可能最好将Bar提升为internal property并设置项目以使内部对您的测试项目可见(使用AssemblyInfo.cs中的InternalsVisibleTo属性)。 The default constructor of Bar could set the property to new Bar() . Bar的默认构造函数可以将属性设置为new Bar() Your test can set it to some subclass of Bar used for testing. 您的测试可以将其设置为用于测试的Bar某个子类。 Or a stub. 或者存根。 I think that will cut down on the amount of changes you'll have to make to make this thing testable going forward. 我认为这将减少你必须做出的改变,以使这个东西可以测试。

And of course you don't need to do any refactoring of a class until you make some other changes to that class. 当然,在对该类进行其他更改之前,您不需要对类进行任何重构。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM