TDD中应该模拟哪些对象?

9
在创建方法时,是否应该将在该方法内实例化的每个对象作为参数传递,以便在我们的单元测试中可以mock这些对象?
我们在工作中有很多没有关联单元测试的方法。回过头来编写测试时,我们发现这些方法内部实例化了相当多的对象。
我们的一个选项是将当前方法重构为更像单元测试的方法,并减少每个方法的职责数量。这可能是一个漫长的过程,但在未来肯定会对我们产生很大的好处。
你认为呢?在方法内实例化的所有对象都应该作为参数传递吗?

在该方法内实例化的每个对象都应该被传递进去。您可能需要重新表述这句话,它很令人困惑。我认为您是在问“而不是在方法内实例化,它们应该被传递进来”。如果将它们视为替代方案,那么它就稍微有些意义了。 - S.Lott
10个回答

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的实现。

6
第一部分有点含糊不清的问题。这就像是问“在用车辆撞到行人时,我应该双手握住方向盘吗?”
实例化大量其他对象的方法几乎肯定做得太多了。具有许多这些方法的类可能没有遵循单一职责原则。
然而,使代码可测试的关键方法之一是使用IoC(控制反转),其中类的依赖关系(或方法的依赖关系)被传递给它,而不是类要求它们。这使得测试变得更加容易,因为您可以传递模拟对象。
因此,简短的回答是“是的”,传递您的依赖项,并查看一个好的 依赖注入组件。长的答案是“是的,但不要那样做”。 DI框架可能会强制您将依赖项传递给对象而不是方法,并且您会发现您想确保限制这些依赖项-这是一件好事。
当然,重构以减少依赖关系是好的。缩短方法以执行一项任务几乎从来不是坏事。只要您能承担短期成本,我强烈同意这是长期利益。

2
只是一个观察:你在谈论方法,而我更喜欢谈论类。
对于这个问题,没有普适的答案。有时候将类的创建和使用解耦非常重要。
考虑以下几点:
- 如果一个类使用另一个类,但你想让它们之间松耦合,你应该使用接口。现在,如果只知道接口而不知道具体类型,第一个类应该如何创建实例? - 解耦类非常重要,但并不是每种情况都需要解耦。如果有疑问,就应该进行解耦。 - 在使用注入时,你需要决定谁来创建和注入实例。你很可能会发现像Spring.Net这样的依赖注入框架非常方便。
还有一个技巧:使注入变成可选的。这意味着,当你在构造函数中传递一个实例时,它将被使用。如果没有,类本身将创建一个新的实例。这在处理遗留代码和没有依赖注入框架时非常方便。

2

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

  1. 传递所有参数 - 就像你已经知道的那样,这种方式可以传递模拟/存根而不是“真实”对象。
  2. 使用IoC容器 - 重构你的代码以使用容器从你的代码中获取对象,并且你可以用模拟/存根替换它们。
  3. 使用Typemock Isolator (.NET),它可以伪造未来的对象 - 即从测试代码中实例化的对象。此选项不需要重构,如果你有一个庞大的代码库,它应该值得尝试。

为了可测试性而设计并不总是一个好习惯,特别是对于已经编写了一些代码的现有项目。 因此,如果你从零开始或者有一个小项目,也许将对象作为参数传递给类的构造函数是可以接受的,只要你没有太多的参数。
- 如果你使用IoC容器。

如果你不想改变你所有现有的代码和/或不想设计你的代码以使其“更具可测试性”(可能会导致一些奇怪的代码),请使用Isolator(或类似工具,如果你使用Java)。


2

只有在编写单元测试时遇到了干扰的对象,才需要模拟这些对象。如果一个方法创建一个对象来执行其任务,并且您可以验证其结果,那么就不需要模拟所创建的对象的类。

当您想要将一个类与其他内容隔离开来时,请使用模拟。使用模拟可使测试远离以下内容:

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

将对象的使用与它们的构造分开。


1

在某些时候,你不得不从一个对象创建另一个对象,但你应该按照良好的设计原则编写软件。例如 SRP、DI 等。

当你有许多依赖关系时,你可能会发现 IoC 容器可以帮助你管理它们。

处理遗留代码时,你可能会发现阅读 Michael Feather 的 《Working Effectively with Legacy code》 很有用。这本书有很多技巧,可以帮助你测试系统。


0

我不确定您使用的是哪种语言/工具,但在Rails中可以这样做,即模拟构造函数:

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

所以从现在开始,在你的测试中如果你调用 User.create(User 是“类”),它将始终返回你预定义的 @user 变量,让你完全控制在单元测试中使用哪些数据。

既然你知道了测试中确切的数据,你可以开始存根对象的实例方法,以确保它们返回适当的数据,这样你就可以用来测试你的方法。


0

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

要有效地做到这一点,通常需要将接口放在语义级别而不是实现级别。


0

你应该优先考虑不带参数的方法,其次是一个参数、两个参数,最后是三个参数。任何需要超过三个参数的方法都是代码异味。要么在传递的所有参数中存在等待被发现的类,要么这个类/方法试图做太多事情。

关于传递依赖项,你可以使用构造函数注入,但随着时间的推移,当你慢慢地开始传递整个对象图时,这会变得难以管理。我的建议是尽早转向使用IoC容器,避免痛苦。


0
我会尝试在类上使用依赖注入,而不是像选定的答案建议的那样由类方法创建对象。当这样做没有意义时,考虑创建一个生产被创建对象的工厂类。然后你可以通过依赖注入传入该工厂。

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