【问题标题】:Combining Unit Tests (mocking) and a Dependecy Injection Framework [duplicate]结合单元测试(模拟)和依赖注入框架[重复]
【发布时间】:2012-03-12 20:52:59
【问题描述】:

可能重复:
Using IoC for Unit Testing

我认为我在理解单元测试和/或依赖注入的工作方式方面确实存在问题。我使用 NUnit 和 Rhino Mocks 进行单元测试,并使用 Ninject 作为 Dependency Incection 框架。总的来说,我认为这两个最适合 - 但不知何故,它似​​乎变得更加复杂和难以理解。

(我会试着编一个很好的例子,让它保持干净和简单。这是关于我的,骑自行车)。

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注入。我选择构造函数注入作为示例:

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&lt;ITransportation&gt;());。这样一来,感觉就像我失去了 DI 的好处 - Person 类可以调用 Travel 而无需任何耦合,并且不需要知道运输是什么类型的类。另外,我认为这种形式缺乏示例 2 的很多可读性。

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

(回想起来,这个例子好像真的很糟糕——大家应该知道他现在骑的是什么样的交通工具……)

【问题讨论】:

  • 我认为您可能对依赖注入 (DI) 和控制反转 (IoC) 的定义感到困惑。第三个图 does 实现了 DI(您已将依赖项移到构造函数中),第二个图是使用 IoC (Ninject) 容器来解决依赖关系。
  • zapthedingbat 是对的。您在第三个示例中进行 DI,但不要使用 DI 容器,这很好。进行 DI 时,DI 容器是可选的。
  • 示例“2”。不使用 DI,而是使用“服务定位器”...

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


【解决方案1】:

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

您的问题始于场景 2:这不是 DI - 这是service locator (anti-)pattern。对于真正的依赖注入,你需要传入你的依赖,最好是通过构造函数注入。

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

我什至会争辩说,如果您需要一个 IoC 容器,那么您的测试可能不够细粒度,或者您有太多的依赖项。在后一种情况下,一些重构可能是为了从您正在使用的两个或多个依赖项中形成聚合类(当然只有在存在任何语义连接的情况下)。这最终会将依赖项的数量降低到您可以接受的水平。这个最大数每个人不一样,我个人力争最多4个,至少我一只手能数出来,嘲讽也不算什么负担。

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

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

【讨论】:

    【解决方案2】:

    你把一些事情弄糊涂了。

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

    所以在测试 3 号时,你会这样做:

    ITransportation transportationMock = MockRepository.GenerateStricktMock<ITransportation>();
    
    // setup exceptations on your mock
    
    var person = new Person(transportationMock);
    

    DI 框架仅在生产代码中构建对象树时才需要。在您的测试代码中,您可以完全控制要测试的内容。当对一个类进行单元测试时,你会模拟出所有的依赖关系。

    如果您还想做一些集成测试,您可以将真正的 Bike 传递给您的 person 类并对其进行测试。

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

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

    【讨论】:

      【解决方案3】:

      我的 2 美分...

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

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

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

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

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-03-03
        • 2021-06-19
        • 1970-01-01
        相关资源
        最近更新 更多