在进行TDD时要模拟哪些对象

时间:2009-11-02 14:30:39

标签: unit-testing tdd mocking

在创建方法时,是否应该将在该方法中实例化的每个对象作为参数传入,以便可以在单元测试中模拟这些对象?

我们在工作中有很多方法,没有相关的单元测试,也没有回顾性地编写测试;我们发现在这些方法中实例化了很多对象。

我们的一个选择是将我们当前的方法重构为更多类似单位的方法,并减少每个方法的责任数量。这可能是一个漫长的过程,但对我们来说肯定是一个很大的好处。

你怎么看?是否应将在方法中实例化的所有对象作为参数传递?

10 个答案:

答案 0 :(得分:7)

也许不是所有对象,但是你注入单位的对象越多,关注点的分离就越好,所以我肯定会建议你向这个方向移动。

您不必将所有对象作为方法参数传递。通过构造函数注入将协作者注入类中通常是更好的设计。这可以使您的界面保持清洁,同时您的实施可以导入所需的协作者。

假设你的原始实现看起来像这样:

public class Foo
{
    public Ploeh DoStuff(Fnaah f)
    {
        var bar = new Bar();
        return bar.DoIt(f);
    }
}

这可以改为看起来像这样:

public class Foo
{
    private readonly IBar bar;

    public Foo(IBar bar)
    {
        this.bar = bar;
    }

    public Ploeh DoStuff(Fnaah f)
    {
        return this.bar.DoIt(f);
    }
}

请注意,我将bar从Bar实例更改为IBar实例,从而将Foo与IBar的特定实现分离。这样的重构往往会使您的单元测试更容易编写并维护,因为您现在可以独立地改变Foo和Bar的实现。

答案 1 :(得分:6)

第一部分是一个有点问题。这就像问“当我的车里的行人跑过来时,我应该把双手放在方向盘上吗?”

实例化大量其他对象的方法几乎肯定做得太多了。具有大量这些方法的类可能不遵循单一责任原则。

然而,使代码可测试的关键方法之一是使用IoC(控制反转),其中将类的依赖(或方法)传递给它,而不是要求它们的类。这样可以更容易测试,因为你可以传入模拟。

所以简短的回答是“是”,传入你的依赖关系并查看一个好的dependency injection组件。长的答案是“是的,但不要这样做”。 DI框架可能会强制您将依赖项传递给对象而不是方法,并且您会发现要确保限制这些依赖项 - 这是一件好事。

当然,重构以减少依赖性是好的。缩短你做一件事的方法几乎从来都不是坏事。我强烈同意这是长期收益,只要你能负担短期费用。

答案 2 :(得分:2)

只是观察:你在谈论方法,而我更喜欢谈论课程。

这个问题没有一般答案。有时,将类的创建与用法分离是非常重要的。

考虑:

  • 如果一个类使用另一个,但你想松散地耦合它们,你应该使用一个接口。现在,如果只知道接口,而不是具体类型,第一个类应该如何创建实例?
  • 解耦类非常重要,但不能也不应该在每种情况下完成。如果有疑问,应该解耦。
  • 使用注入时,您需要确定正在创建和注入实例。您很可能会发现像Spring.Net这样的依赖注入框架会派上用场。

另一个技巧是:使注射可选。这意味着,当您在构造函数中传递实例时,将使用它。如果没有,则由类本身创建一个新实例。这在处理遗留代码时以及没有依赖注入框架时非常方便。

答案 3 :(得分:2)

[免责声明:我在Typemock工作]
你有三个选择 - 其中两个需要一些重构:

  1. 传递所有参数 - 就像你已经知道的那样,你可以传递模拟/存根而不是“真实”对象。
  2. 使用IoC容器 - 重构代码以使用容器从代码中获取对象,并且可以用模拟/存根替换它们。
  3. 使用可以伪造未来对象的Typemock Isolator(.NET) - 在代码中从测试代码中实例化的对象。这个选项不需要重构,如果你有一个很大的代码库,它应该值得它的代码。
  4. 可测试性设计并不总是一个好的做法,尤其是对于现有项目而言 你已经写了一些代码。 因此,如果你开始干净或有一个小项目,也许可以将对象作为参数传递给类的构造函数,只要你没有太多的参数。
    - 如果您使用IoC容器。

    如果您不想更改所有现有代码和/或不希望以某种方式设计代码以使其“更易于测试”(可能导致一些看似奇怪的代码) - 使用Isolator(或类似工具)如果你正在使用Java)。

答案 4 :(得分:2)

仅在编写单元测试时模拟妨碍的对象。如果方法创建了一个对象来执行其任务,您可以探测其结果,那么就不需要模拟所创建的对象的类。

当您想要将一个类与其他类隔离时,请使用mock。使用mock让测试远离

  • 的文件系统
  • 数据库
  • 网络
  • 具有不可预测行为的对象(时钟,随机数生成器......)

将对象与其构造分开使用

答案 5 :(得分:1)

在某些时候,你不会放弃从另一个对象创建,但你应该将软件编写为良好的设计原则。例如。 SRP,DI等..

如果您有许多依赖项,您可能会发现IoC容器将帮助您管理它们。

在处理遗留代码时,您可能会发现阅读Michael Feather的Working Effectively with Legacy code很有用。这本书有很多关于如何让你的系统得到测试的技术。

答案 6 :(得分:0)

我不确定你正在使用哪种语言/工具,但是它可以用来实现它的方式就是模拟构造函数,就像这样:

@user = mock_model(User)
User.stub(:create).and_return(@user)

因此,从现在开始,如果您调用User.create(User是“Class”),它将始终返回您预定义的@user变量,从而可以完全控制单元测试中使用的数据。

现在您已确切知道测试中的数据,您可以开始存根对象的实例方法,以确保它们返回可用于测试方法的正确数据。

答案 7 :(得分:0)

你应该支持不带参数的方法,然后是一个参数,两个,最后三个。任何超过3的东西都是代码味道。要么在传入的所有参数中都有一个等待发现的类,要么类/方法试图做太多。

在传入依赖关系方面,您可以使用构造函数注入,但随着时间的推移,当您慢慢转向必须传递整个对象图时,这会变得难以处理。我的建议是尽快使用IoC容器,避免痛苦。

答案 8 :(得分:0)

我会尝试在类上使用依赖注入,而不是让类方法创建对象(如所选答案推荐的那样)。如果没有意义,可以考虑创建一个生成正在创建的对象的工厂类。然后,您可以通过依赖注入传入该工厂。

答案 9 :(得分:0)

我更喜欢模拟对象周围的所有内容,并根据对相关对象的调用和响应来定义正在测试的对象的行为。

为了有效地做到这一点,通常要求您的接口处于语义级别而不是实现级别。