【问题标题】:Unit testing without dependency injection没有依赖注入的单元测试
【发布时间】:2016-03-20 21:27:58
【问题描述】:

来自 Spring in Action 的代码:

public class DamselRescuingKnight implements Knight {
    private RescueDamselQuest quest;
    public DamselRescuingKnight() {
        this.quest = new RescueDamselQuest();
    }
    public void embarkOnQuest() {
        quest.embark();
    }
}

public class BraveKnight implements Knight {
    private Quest quest;
    public BraveKnight(Quest quest) {
        this.quest = quest;
    }
    public void embarkOnQuest() {
        quest.embark();
    }
}


public class BraveKnightTest {
    @Test
    public void knightShouldEmbarkOnQuest() {
        Quest mockQuest = mock(Quest.class);
        BraveKnight knight = new BraveKnight(mockQuest);
        knight.embarkOnQuest();
        verify(mockQuest, times(1)).embark();
    }
}

我了解依赖注入的使用,它允许我们在不修改依赖代码的情况下切换实现。

这本书说“编写单元测试非常困难......”。

但是,我无法理解如果没有依赖注入进行单元测试会有多么困难!我的直觉拒绝合作!

您能否开始为“DamselRescuingKnight”类和任何其他更好的示例类(没有 DI)编写 junit/单元测试,让我意识到 DI 使单元测试更容易的点/阶段?

【问题讨论】:

  • 您的代码非常简单,因此很难真正解释。如果 quest.embark 是一段复杂的代码,涉及到数据库,涉及用户输入和多台计算机,那么您可能希望用更简单的代码替换复杂的代码。但是,在您的代码中,即使进行单元测试也是没有意义的。 quest.embark 需要测试,但你这里的代码不需要。

标签: java spring unit-testing junit dependency-injection


【解决方案1】:

当您尝试测试DamselRescuingKnight 时,上述示例中的困难就出现了。假设,你想测试那个(见下文)

public class DamselRescuingKnight implements Knight {
    private RescueDamselQuest quest;
    public DamselRescuingKnight() {
        this.quest = new RescueDamselQuest();
    }
    public void embarkOnQuest() {
        quest.embark();
    }
}


public class DamselRescuingKnightTest {
    @Test
    public void knightShouldEmbarkOnQuest() {
        DamselRescuingKnight knight = new DamselRescuingKnight ();
        knight.embarkOnQuest();
        // now what?            
    }
}

你怎么能确定 knight.embarkOnQuest() 确实做了什么?答案是你不能,因为你不能访问它在内部使用的任务实例。

现在,为了能够测试这样的类,您将向 Knight 添加一个 getQuest() 方法,然后向 Quest 添加一个 isEmbarked() 方法。 也很公平地说,这个例子很简单,因为骑士只调用不带参数的任务,没有别的。如果骑士会与任务互动并从铁匠那里获得一些武器,那么您还需要以某种方式允许访问。你可能会做所有的样板来完成它。但是然后假设,您正在将参数传递给铁匠 - 您如何确保传递的参数是正确的?或者你如何确保骑士在去任务之前拿到他/她的武器?

这就是依赖注入发挥作用的地方。您可以只创建模拟(通过使用模拟框架,或通过实现自己的模拟),以便您可以验证您的骑士是否做了预期的事情。

【讨论】:

  • 我可以说我们不应该测试实现细节。 DamselRescuingKnightTest 在内部使用 Quest 对象这一事实是一个实现细节。相反,我们应该只测试公共 API。所以大概 DamselRescuingKnightTest 有一些其他的方法,比如getQuestResult(),我们会测试这些。
【解决方案2】:

问题当然是quest 变量。您想以某种方式检查是否调用了 embark() 方法。如果无法用模拟实例替换它,这非常困难。

如果变量是protected 而不是private,则测试用例可能会因为位于同一个包中而覆盖它。

您还可以使用面向方面的编程来替换变量。

但最简单的方法是从一开始就使用依赖注入编写代码。

您要求了解如何使用 AOP。以下是一个 AspectJ 切入点的示例,您可以在单元测试中使用该切入点将 RescueDamselQuest 实例替换为一个名为 MockRescueDamselQuest 的模拟实例(抱歉,如果我没有完全正确地理解语法,已经有一段时间了因为我使用了 AspectJ):

aspect MockRescueDamselQuestInstantiations {
    RescueDamselQuest around (): call(RescueDamselQuest.new()) {
        return new MockRescueDamselQuest();
    }
}

这将捕获RescueDamselQuest 的任何实例化(即对new RescueDamselQuest() 的调用)并返回MockRescueDamselQuest 对象。

考虑到这需要更多的布线,我强烈建议改用依赖注入!

【讨论】:

  • 唯一合理的测试是检查quest.embark() 是否在embarkOnQuest() 被调用时被调用,在这种情况下@James 指出。你说没有模拟实例很难。你能试着做并证明这很难吗?它会给我一个视角。如果我以某种方式故意避免 DI,你说它可以通过 AOP 来完成。你能举一个这样的例子吗?
【解决方案3】:

当我在 Spring in Action 中阅读这篇文章时,这也让我感到困惑。阅读上述答案后,我想补充一点,当不使用 DI 时,Junit 方法需要调用私有对象的方法(可访问),并且此对象 Quest 是在 DamselRescuingKnight 的构造函数中创建的,因此用于 boardQuest() 的测试用例可以不写。相反,当使用 DI 时,您正在外部化对象创建,而 Junit 方法可以创建该对象,以便它可以访问它,然后可以测试 emabarkQuest() 最终需要测试 quest 方法

【讨论】:

    猜你喜欢
    • 2021-06-19
    • 1970-01-01
    • 2017-04-01
    • 2021-05-29
    • 2018-03-03
    • 2017-02-07
    • 1970-01-01
    相关资源
    最近更新 更多