依赖注入和单位测试
#tdd #测试 #java

任何开发人员都使用面向对象的编程语言并为实现的单元测试写作,都可以选择如何为课程提供依赖项。某些语言几乎没有选择,有些语言仅提供了通过构造函数参数的一种方式。

让我们以Java语言为例,在这种情况下,可以通过两种方式提供依赖项,并且在两种情况下,编写单元测试都可以不同。每个人都可以使用哪些优点和缺点。

所以,我们的任务看起来像:

  • 必须通过相应实例分配类字段;
  • 应该写单位测试。

这是我们要修改的初始代码段。代码段具有一个依赖性,要在运行时填充。

public class Service {

    Dependency dependency;

    public void action() {
        dependency.action();
    }

}

通过DI框架将依赖项分配给课堂字段

如果我们使用例如Spring框架,则可以在这样的东西中更改以下示例。

@Component
public class Service {

    @Autowired
    private Dependency dependency;

    public void action() {
        dependency.action();
    }

}

看起来很清晰,没有“多余的”。很少有东西困扰我。

第一件事 - Java反射用于分配依赖项。没什么大不了的,但是在背景中以某种方式为私有字段提供价值并不是很简单和干净 - 仅从课程中分配私有字段。

第二件事 - 班级的字段不是最终的。从技术上讲,这意味着可以在班级实例的生命周期中从类内部修改字段。换句话说,班级部分失去了不变性,这是不好的 - 状态的变化和行为也会改变。编写良好和干净的单元测试将很难。

好吧,到目前为止,它看起来还不错,但是让我们尝试为上述实现编写单元测试。

这是单位测试的任务:

  • 将依赖项分配给类字段;
  • 检查依赖关系的方法是否实际上是调用的。

当前实现不假设依赖关系直接分配给类字段,因此我们将使用DI将依赖项的实例分配给私人类字段。只能仅使用mockito扩展或mockito + Spring testing扩展程序来完成此操作。每种方式都有利弊,但结果将分配给所需字段,我们能够编写单元测试以验证我们的实现。

@ExtendWith(MockitoExtension.class)
class ServiceTest {

    @Mock
    private Dependency dependency;

    @InjectMocks
    private Service subject;

    @Test
    public void testAction() {
        subject.action();

        verify(dependency, times(1)).action();
    }

}

我会强调的是,这是非常平稳的方法:初始化和分配是看不见的。唯一的一件事 - 我们需要一个调解人才能嘲笑并将实例分配给课堂字段。我们看不到并控制嘲笑和分配发生的方式和何时发生。这个事实已经使这种方法有些雾gy:看起来我们“看到”了一切,但并不清楚。雾中的形状可能很棘手;)

通过DI框架通过类构造函数将依赖项分配给类字段

让我们检查一下通过类构造函数提供依赖项的另一种方法。样本Java类可以以以下形式进行转换。

@Component
public class Service {

    private final Dependency dependency;

    public Service(Dependency dependency) {
        this.dependency = dependency;
    }

    public void action() {
        dependency.action();
    }

}

所有依赖性都是通过构造函数明确提供的,类字段标记为final,并且在类实例的整个生命周期中都是不变的。也许构造函数在类定义中会占用更多空间,因为我们将其用于分配依赖项。

让我们编写一个单元测试以实现此类实现,并遵循与通过DI直接到类字段的接线依赖关系相同的任务:

  • 将依赖项分配给类字段;
  • 检查依赖关系的方法是否实际调用。
class ServiceTest {

    private final Dependency dependency = mock(Dependency.class);

    private final Service subject = new Service(dependency);

    @Test
    public void testAction() {
        subject.action();

        verify(dependency, times(1)).action();
    }

}

由于我们通过构造函数将依赖项传递给了一个类,因此我们可以在下面使用以下方法。测试实现不需要任何特殊的扩展即可运行测试:依赖关系和测试主题创建“手动”发生。这种实现没有隐形的动作,一切都由我们控制。实现以不同的方式看待,可以看到依赖性创建,也可以看到传递依赖关系,代码比以前的实现更长。肯定会更明显的是,这里会发生什么。

为什么final在这里很重要?

应用于类字段的关键字final表示不变性。如果类字段没有这样的关键字,则有可能修改类字段的值。这意味着在某些特定情况下,我们的班级在技术上的行为可能会有所不同,而且根本不好 - 这是一种产生生产问题而不是偏爱的方式。由于一类失去不变性,因此单位测试结果变得不可预测,可以描述为flaky。因此,final制造了一个对象immutable,单位测试产生相同的结果,并且在生产中的应用表现与原本。

结论

单元测试的基本部分是简单性和稳定性:)简单性有助于简单地理解实施,
稳定性在多次调用中提供了相同的结果,并且都导致生产中的稳定工作。考虑到上述方法,我个人会选择第二种方法。就我而言,它看起来更简单,干净和直截了当。第二个在测试中没有“魔术” - 所有内容都在桌子上。