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

7

可能是重复的问题:
使用IoC进行单元测试

我认为我在理解单元测试和依赖注入的工作方式方面存在问题。我正在使用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 方法是否被正确调用,我需要一个 Mock。据我所知,通常有两种方式来注入接口:构造函数注入Setter 注入。在我的示例中,我选择构造函数注入:

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中的可读性。
这就是做法吗?还是有其他更优雅的方法来实现依赖注入和进行单元测试(和模拟)的可能性?
(回头看,似乎这个例子真的很糟糕 - 每个人都应该知道他目前骑的是什么交通工具...)

我认为你可能对依赖注入(DI)和控制反转(IoC)的定义感到困惑。你的第三个图表确实实现了DI(你将依赖项移到了构造函数中),而你的第二个图表则使用了IoC(Ninject)容器来解决依赖关系。 - Sam Greenhalgh
zapthedingbat 是正确的。在第三个例子中,您正在进行 DI,但只是不使用 DI 容器,这是可以的。在进行 DI 时,DI 容器是可选的。 - Steven
1
  1. 这里没有使用 DI,而是使用了“服务定位器”...
- Rogério
3个回答

11

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

您的问题始于场景2:这不是DI-这是服务定位器(反)模式。对于真正的依赖注入,您需要通过构造函数注入来传递依赖项。

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

我甚至会认为,如果你需要一个控制反转容器,那么你的测试可能不够细粒度或者你有太多依赖关系。在后一种情况下,可能需要进行一些重构,将您正在使用的两个或多个依赖项形成聚合类(当然只有在存在任何语义连接时)。这将最终降低您所满意的依赖关系数量。每个人的最大数量都不同,我个人努力保持最多4个,至少可以用一只手数出它们,并且模拟不会太麻烦。
使用IoC容器进行单元测试的最后一个重要论点是行为测试:如果您无法完全控制依赖项,如何确保要测试的类表现出您想要的行为?
可以通过使用设置某些操作标志的类型来存根化所有依赖项来实现此目的,但这是一项巨大的工作。使用RhinoMocks或Moq等模拟框架验证指定参数调用了某些方法要简单得多得多。为此,您需要模拟要验证调用的依赖项,IoC容器不能帮助您。

4
你有些事情搞混了。
实现3比2更好,因为你不需要在单元测试中设置DI框架。
所以当测试第3个时,你会这样做:
ITransportation transportationMock = MockRepository.GenerateStricktMock<ITransportation>();

// setup exceptations on your mock

var person = new Person(transportationMock);

DI框架仅在生产代码中构建对象树时才需要使用。在测试代码中,您可以完全控制想要测试的内容。在单元测试类时,您需要模拟所有依赖项。

如果您还想进行一些集成测试,则应将真实的自行车传递给您的人类类并进行测试。

完全隔离地测试类的想法是您可以控制每个代码路径。您可以使依赖关系返回正确或不正确的值,甚至可以使它引发异常。如果一切正常,并且您有从单元测试获得的良好代码覆盖率,那么您只需要进行几个更大的测试就可以确保您的DI已正确连接。

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


3

我的看法...

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

你的第3点说明了依赖注入,在Person的构建过程中,IoC容器会将依赖项(ITransportation)注入到构造函数中。

在实际应用程序和单元测试中,您也需要使用IoC容器来构建Person(即不要直接创建Person)。可以使用服务定位模式(kernel.Get<Person>();),或者如果您的单元测试框架支持此功能,可以使用DI(如Setter)。

这样就可以构建Person及其依赖项(即为ITransportation配置的具体类),并将其注入到Person中(显然,在单元测试中,您的IoC将为模拟/存根的ITransportation进行配置)。

最后,您需要模拟依赖项,即ITransportation,以便测试Person的Transport()方法。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接