结合单元测试(模拟)和依赖注入框架

时间:2012-02-22 14:02:09

标签: c# unit-testing dependency-injection mocking ninject

  

可能重复:
  Using IoC for Unit Testing

我认为我确实理解单元测试和/或依赖注入的工作方式。我正在使用NUnit和Rhino Mocks进行单元测试,将Ninject用作依赖性感知框架。总的来说,虽然我认为这两种方式完全合适 - 但不知何故,它似​​乎变得更加复杂和难以理解。

(我会尝试做一个很好的例子,让它保持干净和轻松。这是关于我,骑自行车)。

1。)没有DI /单元测试:
如果不知道DI和单元测试,我的代码看起来就像那样 - 我会很开心:

public class Person
{
    public void Travel()
    {
        Bike bike = new Bike();
        bike.Ride();
    }
}

public class Bike
{
    public void Ride()
    {
        Console.WriteLine("Riding a Bike");
    }
}

骑自行车我只需要:new Person().Travel();

2。)DI:
我不想要紧耦合,所以我需要一个接口和一个NinjectModule!我有一些开销,但只要代码易于阅读和理解,那就没问题了。我只是传递修改后的Person类的代码,Bike类没有改变:

public class Person
{
    IKernel kernel = new StandardKernel(new TransportationModule());
    public void Travel()
    {
        ITransportation transportation = kernel.Get<ITransportation>();
        transportation.Ride();
    }
}

我仍然可以用new Person().Travel();

骑自行车

3。)考虑单元测试(无DI):
为了能够检查是否正确调用了Ride-Method,我需要一个Mock。据我所知,有两种注入接口的方法:构造函数注入 Setter Injection 。我为我的例子选择了Constructor Injection:

public class Person
{
    ITransportation transportation;

    public person(ITransportation transportation)
    {
        this.transportation = transportation;
    }

    public void Travel()
    {
        transportation.Ride();
    }
}

这一次,我想通过自行车:new Person(new Bike()).Travel();

4。)使用DI并准备单元测试
3中的类。考虑单元测试(没有DI)无需修改即可完成工作,但我需要调用new Person(kernel.Get<ITransportation>());。通过这种感觉,我觉得我正在失去DI的好处 - Person类可以在没有任何耦合的情况下调用Travel,并且需要知道什么类型的交通工具。另外,我认为这种形式缺乏示例2的可读性。

这是怎么做的?或者还有其他 - 更优雅的方式实现依赖注入和单元测试(和模拟)的可能性?

(回想起来,看起来这个例子真的很糟糕 - 每个人都应该知道他现在骑的是什么样的交通工具......)

3 个答案:

答案 0 :(得分:11)

通常我会尽量避免使用IoC容器进行单元测试 - 只需使用模拟和存根来传递依赖项。

您的问题从方案2开始:这是 DI - 这是service locator (anti-)pattern。对于真正的Dependency Injection ,您需要传入依赖项,最好是通过构造函数注入。

场景3看起来不错,这是DI,通常也是如何在隔离中测试您的类 - 传递您需要的依赖项。我很少发现需要使用完整的DI容器进行单元测试,因为每个被测试的类只有几个依赖项,每个依赖项都可以被存根或模拟以执行测试。

我甚至会争辩说,如果你需要一个IoC容器,你的测试可能不够精细,或者你有太多的依赖。在后一种情况下,一些重构可能是为了从您正在使用的两个或多个依赖项中形成聚合类(当然,只有在存在任何语义连接时)。这最终会将依赖项数量降低到您认为合适的水平。每个人的最大数量是不同的,我个人努力最多只有4个,至少我一方面可以算数,嘲笑不是太多的负担。

在单元测试中使用IoC容器的最后一个重要论点是行为测试:如果您没有完全控制,如何确保被测试的行为符合您的要求?您的依赖项?

可以说,你可以通过使用为某些操作设置标志的类型来删除所有依赖项来实现这一点,但这是一项很大的工作。使用像RhinoMocks或Moq这样的模拟框架验证使用您指定的参数调用某些方法要容易得多。为此,您需要模拟要验证调用的依赖项,IoC容器无法帮助您。

答案 1 :(得分:4)

你有些困惑。

实现3比2更好,因为您不需要在单元测试中设置DI框架。

因此,在测试3号时你会这样做:

ITransportation transportationMock = MockRepository.GenerateStricktMock<ITransportation>();

// setup exceptations on your mock

var person = new Person(transportationMock);

DI框架只是在生产代码中构造对象树时才需要的。在测试代​​码中,您可以完全控制要测试的内容。单元测试一个类时,你会模拟所有依赖项。

如果您还想进行一些集成测试,您可以将真正的自行车传递给您的人员班并进行测试。

在完全隔离中测试类的想法是,您可以控制每个代码路径。您可以使依赖项返回正确或不正确的值,或者甚至可以让它抛出异常。如果一切正常,并且您的单元测试只有很好的代码覆盖率,那么您只需要进行几项更大的测试即可确保正确连接DI。

编写可测试代码的关键是从业务逻辑中分离对象创建。

答案 2 :(得分:3)

我的2美分......

虽然第2点是依赖性倒置原则(DIP)的一个例子,但它使用的是服务位置模式,而不是依赖注入。

您的观点3说明了依赖注入,IoC容器会在构建Person期间将依赖项(ITransportation)注入构造函数。

在您的真实应用程序和单元测试中,您还希望使用IoC容器来构建Person(即不要直接使用新的Person)。如果您的单元测试框架支持此功能,请使用服务定位器模式(kernel.Get<Person>();)或DI(例如Setter)。

这将构建Person及其依赖关系(即ITransportation的Configured具体类)并将其注入到Person中(显然,在单元测试中,您的IoC将配置为模拟/存根ITransportation)

最后,你想要Mock的依赖关系,即ITransportation,这样你就可以测试Person的Transport()方法。

由于Bike没有依赖关系,它可以直接/独立地进行单元测试(除非向Bike添加依赖项,否则不需要模拟测试Bike.Ride())。