在 Java 中使用 toString() 进行单元测试

Using toString() for unit testing in Java

在单元测试中,根据 toString() 的字符串测试返回值通常是个好主意吗 returns?

例如,执行以下操作以确保返回预期的列表:

 assertEquals(someExpression.toString() ,"[a, b, c]");

在我看来,有以下几点考虑:

优点:节省时间(构造实际期望值需要更长的代码)。

缺点: 测试依赖于 toString(),它在文档中没有正式定义,因此可以在任何未来版本中更改。

最好覆盖 equalshashCode(您自己或例如为此使用 lomboks @EqualsAndHashCode)并使用它们进行比较,而不是使用 toString .这样您就可以明确地进行关于相等性的测试,稍后您可以重用这些方法。

然后你可以这样做(按照你的列表示例):

assertEquals(
    someExpression,
    Arrays.asList(new Foo("a"), new Foo("b"), new Foo("c"))
);

Assert.assertThat() 方法具有特定于集合的匹配器等等。

例如,contains() 适用于集合:

List<String> someExpression = asList("a", "b", "c");
assertThat(someExpression.toString(), contains("a", "b", "c"));

优点是:

  • 您不需要实施 .toString();
  • 当测试失败时,错误信息描述性很强,会说 "Expected collection containig 'b' and didn't find it."

尝试:

import static org.junit.Assert.assertThat;
import static org.hamcrest.Matchers.contains;
import static java.util.Arrays.asList;

List<String> someExpression = asList("a", "c");
assertThat(someExpression.toString(), contains("a", "b", "c"));

我唯一一次测试一个对象的 toString() 是当我有一个不可编辑的 class 时,它没有实现 hashcodeequals,而是实现了 toString() 来输出其字段的内容。即使那样,我也不会使用硬编码字符串作为相等性测试,而是做类似

的事情
SomeObject b = new SomeObject(expected, values, here);
assertEquals(a.toString(), b.toString());

您的方法可能会最初节省时间,但从长远来看运行仅仅维护测试就需要更多时间,因为您正在对字符串进行硬编码toString().

的预期结果

编辑 1:当然,如果您正在测试输出字符串的函数/进程,那将是您应该按预期使用硬编码字符串的情况之一结果。

String input = "abcde";
String result = removeVowels(input);
assertEquals(result, "bcd");

我在特定情况下使用 toString,尤其是当等效代码要复杂得多时。随着您获得更复杂的数据结构,您需要一种快速的方法来测试整个结构。如果您逐个字段编写代码进行测试,您可能会忘记添加一个字段。

以我这里为例 https://vanilla-java.github.io/2016/03/23/Microservices-in-the-Chronicle-world-Part-1.html

TopOfBookPrice tobp = new TopOfBookPrice("Symbol", 123456789000L, 1.2345, 1_000_000, 1.235, 2_000_000);
assertEquals("!TopOfBookPrice {\n" +
        "  symbol: Symbol,\n" +
        "  timestamp: 123456789000,\n" +
        "  buyPrice: 1.2345,\n" +
        "  buyQuantity: 1000000.0,\n" +
        "  sellPrice: 1.235,\n" +
        "  sellQuantity: 2000000.0\n" +
        "}\n", tobp.toString());

这个测试失败了,为什么?你可以很容易地在你的 IDE 中看到它产生

随着您获得更复杂的示例,检查每个(嵌套的)值并稍后在它中断时更正它真的很乏味。更长的例子是

assertEquals("--- !!meta-data #binary\n" +
        "header: !SCQStore {\n" +
        "  wireType: !WireType BINARY,\n" +
        "  writePosition: 0,\n" +
        "  roll: !SCQSRoll {\n" +
        "    length: !int 86400000,\n" +
        "    format: yyyyMMdd,\n" +
        "    epoch: 0\n" +
        "  },\n" +
        "  indexing: !SCQSIndexing {\n" +
        "    indexCount: !short 16384,\n" +
        "    indexSpacing: 16,\n" +
        "    index2Index: 0,\n" +
        "    lastIndex: 0\n" +
        "  },\n" +
        "  lastAcknowledgedIndexReplicated: -1,\n" +
        "  recovery: !TimedStoreRecovery {\n" +
        "    timeStamp: 0\n" +
        "  }\n" +
        "}\n" +
        "# position: 344, header: 0\n" +
        "--- !!data #binary\n" +
        "msg: Hello world\n" +
        "# position: 365, header: 1\n" +
        "--- !!data #binary\n" +
        "msg: Also hello world\n", Wires.fromSizePrefixedBlobs(mappedBytes.readPosition(0)));

我在执行此操作时有更长的示例。我不需要为我检查的每个值都写一行,如果格式发生变化,我什至知道一点。我不希望格式意外更改。

注意:我总是把期望值放在第一位。

Using toString() for unit testing in Java

如果在单元测试中,您的意图是断言对象的所有字段都相等,则被测对象的 toString() 方法必须返回一个显示所有字段的键值的字符串。
但是正如您强调的那样,在当时,class 的字段可能会发生变化,因此您的 toString() 方法可能无法反映实际字段,并且 toString() 方法并非为此设计。

The test depends on toString(), which is not formally defined in the doc, and thus can change in any future version.

toString() 的替代方案,可以在没有
的情况下编写漂亮的代码 约束您维护一个 toString() 方法并返回所有键值字段或混合 toString() 用于调试目的的初始意图与断言目的。
此外,单元测试应该记录被测试方法的行为。
以下代码无法对预期行为进行自我解释:

 assertEquals(someExpression.toString() ,"[a, b, c]");

1) 反射库

这个想法是将Java反射机制JUnit断言机制(或您使用的任何测试单元API)混合.
通过反射,您可以比较两个相同类型对象的每个字段是否相等。这个想法如下:您构建一个错误文本消息,指示两个字段之间不遵守相等性的字段以及原因。
当通过反射比较所有字段时,如果错误消息不为空,则使用单元测试机制抛出失败异常,并带有您之前构建的相关错误消息。
否则,断言是成功的。 当它被证明是相关的时,我会定期在我的项目中使用它。
您可以自己完成,也可以使用 API 之类的 Unitils,它可以为您完成这项工作。

User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
ReflectionAssert.assertReflectionEquals(user1, user2);

就个人而言,我创建了自己的库来完成这项工作,并有可能在断言中添加多个自定义项。 这并不难,您可以从 Unitils 中获得灵感。

2) 单元测试匹配库

我认为这是一个更好的选择。
您可以使用 Harmcrest 或 AssertJ。
它比纯反射更冗长,但它也有很大的优势:

  • 它专门记录了测试方法预期的行为。
  • 它提供了流畅的断言方法
  • 它提供多个断言功能以减少单元测试中的样板代码

使用 AssertJ,您可以替换此代码:

 assertEquals(foo.toString() ,"[value1=a, value1=b, value1=c]");

其中 foo 是 class 的 Foo 实例,定义为:

public class Foo{

  private String value1;
  private String value2;
  private String value3;

  // getters
}

如:

Assertions.assertThat(someExpression)
          .extracting(Foo::getValue1, Foo::getValue2, Foo::getValue3)
          .containsExactly(a, b, c);