单元测试的相互依赖

时间:2018-02-05 21:56:38

标签: java unit-testing junit

最近,我正在尝试进行单元测试,以熟悉这种做法,并最终编写更好的代码。我已经开始了我的一个大项目,有一个相当大的未经测试的代码库,但我只是单元测试实用程序类,它没有很多依赖项并且很容易测试。

我已经阅读了一般的单元测试的相当多的介绍性材料,经常出现的是单元测试应该始终测试尽可能小的行为单元。也就是说,测试通过还是失败应仅依赖于一段非常具体的代码,例如一种小方法。

然而,我已经发现将这个想法付诸实践的问题。考虑例如:

@Test
public void testSomethingWithFoo() {
    Foo foo = new Foo(5);

    Bar result = foo.bar(); //Suppose this returns new Bar(42) for some reason.

    Bar expected = new Bar(42);
    Assert.assertEquals(expected, result); // Implicitly calls Bar.equals, which returns true if both Bars' constructor parameters are equal.
}

显然,这个测试依赖于两个元素:foo.bar()方法,还有bar.equals(otherBar)方法,它由断言隐式调用。

现在,我可以编写另一个测试,断言这个bar.equals()方法正常工作。但是,说它失败了。现在,我的第一次测试也应该失败,但出于超出其范围的原因。

我的观点是,对于这个特殊的例子,没有什么是真正有问题的;我可以在不使用equals的情况下检查相等性。但是,我觉得这类问题将成为一个更复杂的行为的真正问题,因为它们可能不起作用而避免使用现有方法会涉及为测试重写大量代码。

如何在不使单元测试相互依赖的情况下将这些需求转换为代码?

甚至可能吗?如果没有,我是否应该停止对此问题的困扰,并假设所有不在当前测试下的方法都有效?

2 个答案:

答案 0 :(得分:1)

  

然而,说它失败了。现在,我的第一次测试也应该失败,但对于一个   超出其范围的原因。

测试隔离是非常重要的,你是对的,但也要注意这有一个限制 在某些情况下,您将需要依赖于其他对象的其他常规方法,例如equals / hashCode或仍然是构造函数。 这是不可避免的。有时,耦合不是问题 例如,使用构造函数来创建传递给被测方法的对象或模拟值是很自然的,真的不应该避免!

但在其他情况下,与其他类的API的耦合是不可取的 例如,您的示例代码就是这样的情况,因为您的测试依赖于equals()方法,测试的字段可能随时间而变化,并且在功能上与测试方法中要监视/断言的字段不同。
此外,这不会使字段显式断言。

在这种情况下,要通过测试方法断言返回对象的字段值,您应该通过getter方法逐字段断言。
为了避免编写这个重复且容易出错的代码,我使用了匹配器测试库,例如AssertJHamcrest(一次包含在JUnit中),它提供了一种流畅而灵活的方式来断言实例的内容。

例如AssertJ:

Foo foo = new Foo(5);
Bar actualBar = foo.bar();
Assertions.assertThat(actualBar)
          .extracting(Bar::getId) // supposing 42 is stored in id field              
          .containsExactly(42);

通过这样一个简单的例子,匹配器测试库没有实际价值。你可以直接做:

Foo foo = new Foo(5);
Bar actualBar = foo.bar();
Assert.assertEquals(42, actualBar.getId())

但是对于您想要检查多个字段的情况(非常常见的情况),它非常有用:

Foo foo = new Foo(5);
Bar actualBar = foo.bar();
Assertions.assertThat(actualBar)
          .extracting(Bar::getId, Bar::getName, Bar::getType)                
          .containsExactly(42, "foo", "foo-type);

答案 1 :(得分:0)

我发现将我的班级划分为两种主要类型很有帮助:

  1. 包含数据的类型。它们代表了数据的结构,几乎没有逻辑。
  2. 持有逻辑且不持有状态的类型。
  3. 这样做的原因是许多层的许多类都不可避免地与您的数据模型相关联,因此您希望数据模型的表示形式坚如磐石,不会产生任何意外。

    如果Bar是数据结构,那么依赖其.equals()方法的行为与依赖int或{{1}的行为完全不同{'} String行为。你应该不要担心它。

    另一方面,如果.equals()是逻辑类,那么在您尝试测试Bar时,您可能根本不应该依赖其行为。事实上,Foo根本不应该创建Foo(除非BarFoo,例如)。相反,它应该依赖于可以模拟的接口。

    在测试数据类型时(特别是在数据类型确实需要某些逻辑的特殊情况下,比如Uri类,您必须在其中测试多个方法那个类(比如Bar的构造函数和equals方法)。但测试应该都是专门测试那个类。确保你有足够的测试来保持这些坚如磐石。

    在测试服务类(具有逻辑的服务类)时,您将有效地假设您正在处理的任何数据类型已经过充分测试,因此您可以真正测试您所使用的类型的行为。 ;担心(BarFactory),而不是你碰巧与之互动的其他类型(Foo)。