1. 概述

Truth 是一个流畅且灵活的开源测试框架,旨在让测试断言和失败信息更具可读性。

本文将深入探讨 Truth 框架的核心特性,并通过示例展示其强大功能。

2. Maven 依赖

首先,在 pom.xml 中添加 truthtruth-java8-extension 依赖:

<dependency>
    <groupId>com.google.truth</groupId>
    <artifactId>truth</artifactId>
    <version>0.32</version>
</dependency>
<dependency>
    <groupId>com.google.truth.extensions</groupId>
    <artifactId>truth-java8-extension</artifactId>
    <version>0.32</version>
    <scope>test</scope>
</dependency>

最新版本可在 Maven Central 查找:

3. 框架介绍

Truth 支持多种类型的可读断言和失败信息:

  • 标准 Java 类型 – 基本类型、数组、字符串、对象、集合、异常、类等
  • Java 8 类型OptionalStream 实例
  • Guava 类型OptionalMultimapMultisetTable 对象
  • 自定义类型 – 通过扩展 Subject 类实现(后文详述)

通过 TruthTruth8 类,库提供了针对被测对象(subject)的断言工具方法。一旦确定 subject 类型,Truth 能在编译时推断出该对象支持的断言方法。例如:

  • 对 List 断言时返回 IterableSubject,提供 contains() 等方法
  • 对 Map 断言时返回 MapSubject,提供 containsEntry() 等方法

4. 快速上手

导入 Truth 的入口点开始编写断言:

import static com.google.common.truth.Truth.*;
import static com.google.common.truth.Truth8.*;

创建示例类(后续示例使用):

public class User {
    private String name = "John Doe";
    private List<String> emails = Arrays.asList("john.doe@example.com", "john.doe2@example.com");

    public boolean equals(Object obj) {
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        User other = (User) obj;
        return Objects.equals(this.name, other.name);
    }
    
    // 标准构造器、getter/setter
}

注意自定义 equals() 方法:仅当 name 相同时判定两个 User 相等。

5. 标准 Java 类型断言

5.1 Object 断言

Truth 提供 Subject 包装器处理对象断言。它是所有包装器的父类,核心方法包括:

@Test
public void whenComparingUsers_thenEqual() {
    User aUser = new User("John Doe");
    User anotherUser = new User("John Doe");
    assertThat(aUser).isEqualTo(anotherUser);
}

其他常用断言:

@Test
public void whenComparingUser_thenInList() {
    User aUser = new User();
    assertThat(aUser).isIn(Arrays.asList(1, 3, aUser, null));
}

@Test
public void whenComparingUser_thenNotInList() {
    User aUser = new User();
    assertThat(aUser).isNotIn(Arrays.asList(1, 3, "Three"));
}

@Test
public void whenComparingUser_thenIsNull() {
    User aUser = null;
    assertThat(aUser).isNull();
}

@Test
public void whenComparingUser_thenInstanceOf() {
    User aUser = new User();
    assertThat(aUser).isInstanceOf(User.class);
}

⚠️ 所有 Subject 的方法都适用于子类,但后续章节聚焦各类型的专属方法。

5.2 数值类型断言(Integer/Float/Double)

支持等值、大小比较:

@Test
public void whenComparingInteger_thenEqual() {
    int anInt = 10;
    assertThat(anInt).isEqualTo(10);
}

@Test
public void whenComparingFloat_thenIsBigger() {
    float aFloat = 10.0f;
    assertThat(aFloat).isGreaterThan(1.0f);
}

@Test
public void whenComparingDouble_thenIsSmaller() {
    double aDouble = 10.0;
    assertThat(aDouble).isLessThan(20.0);
}

浮点数精度断言:

@Test
public void whenComparingDouble_thenWithinPrecision() {
    double aDouble = 22.18;
    assertThat(aDouble).isWithin(2).of(23d);
}

@Test
public void whenComparingFloat_thenNotWithinPrecision() {
    float aFloat = 23.04f;
    assertThat(aFloat).isNotWithin(1.3f).of(100f);
}

5.3 BigDecimal 断言

支持忽略 scale 的比较:

@Test
public void whenComparingBigDecimal_thenEqualIgnoringScale() {
    BigDecimal aBigDecimal = BigDecimal.valueOf(1000, 3);
    assertThat(aBigDecimal).isEqualToIgnoringScale(new BigDecimal(1.0));
}

5.4 Boolean 断言

仅提供两个核心方法:

@Test
public void whenCheckingBoolean_thenTrue() {
    boolean aBoolean = true;
    assertThat(aBoolean).isTrue();
}

5.5 String 断言

支持前缀、包含、后缀等检查:

@Test
public void whenCheckingString_thenStartsWith() {
    String aString = "This is a string";
    assertThat(aString).startsWith("This");
}

其他方法(contains()endsWith()isEmpty())详见源码。

5.6 数组断言

支持等值和空数组检查:

@Test
public void whenComparingArrays_thenEqual() {
    String[] first = { "one", "two", "three" };
    String[] second = { "one", "two", "three" };
    assertThat(first).isEqualTo(second);
}

@Test
public void whenCheckingArray_thenEmpty() {
    Object[] anArray = {};
    assertThat(anArray).isEmpty();
}

5.7 Comparable 断言

支持范围和列表检查:

@Test
public void whenCheckingComparable_thenAtLeast() {
    Comparable<Integer> aComparable = 5;
    assertThat(aComparable).isAtLeast(1);
}

@Test
public void whenCheckingComparable_thenInRange() {
    Comparable<Integer> aComparable = 5;
    assertThat(aComparable).isIn(Range.closed(1, 10));
}

@Test
public void whenCheckingComparable_thenInList() {
    Comparable<Integer> aComparable = 5;
    assertThat(aComparable).isIn(Arrays.asList(4, 5, 6));
}

等价性检查(需实现 Comparable):

public class User implements Comparable<User> {
    // ...
    public int compareTo(User o) {
        return this.getName().compareToIgnoreCase(o.getName());
    }
}

@Test
public void whenComparingUsers_thenEquivalent() {
    User aUser = new User(); aUser.setName("John Doe");
    User anotherUser = new User(); anotherUser.setName("john doe");
    assertThat(aUser).isEquivalentAccordingToCompareTo(anotherUser);
}

5.8 Iterable 断言

核心断言包括:

@Test
public void whenCheckingIterable_thenContains() {
    List<Integer> aList = Arrays.asList(4, 5, 6);
    assertThat(aList).contains(5);
}

@Test
public void whenCheckingIterable_thenContainsAnyInList() {
    List<Integer> aList = Arrays.asList(1, 2, 3);
    assertThat(aList).containsAnyIn(Arrays.asList(1, 5, 10));
}

@Test
public void whenCheckingIterable_thenContainsExactElements() {
    List<String> aList = Arrays.asList("10", "20", "30");
    List<String> anotherList = Arrays.asList("10", "20", "30");
    assertThat(aList)
      .containsExactlyElementsIn(anotherList)
      .inOrder();
}

自定义排序检查:

@Test
public void givenComparator_whenCheckingIterable_thenOrdered() {
    Comparator<String> aComparator = (a, b) -> new Float(a).compareTo(new Float(b));
    List<String> aList = Arrays.asList("1", "012", "0020", "100");
    assertThat(aList).isOrdered(aComparator);
}

5.9 Map 断言

支持条目、键值检查:

@Test
public void whenCheckingMap_thenContainsEntry() {
    Map<String, Object> aMap = new HashMap<>();
    aMap.put("one", 1L);
    assertThat(aMap).containsEntry("one", 1L);
}

@Test
public void whenCheckingMap_thenContainsKey() {
    Map<String, Object> map = new HashMap<>();
    map.put("one", 1L);
    assertThat(map).containsKey("one");
}

@Test
public void whenCheckingMap_thenContainsEntries() {
    Map<String, Object> aMap = new HashMap<>();
    aMap.put("first", 1L); aMap.put("second", 2.0); aMap.put("third", 3f);
    Map<String, Object> anotherMap = new HashMap<>(aMap);
    assertThat(aMap).containsExactlyEntriesIn(anotherMap);
}

5.10 Exception 断言

支持异常原因和消息检查:

@Test
public void whenCheckingException_thenInstanceOf() {
    Exception anException = new IllegalArgumentException(new NumberFormatException());
    assertThat(anException)
      .hasCauseThat()
      .isInstanceOf(NumberFormatException.class);
}

@Test
public void whenCheckingException_thenCauseMessageIsKnown() {
    Exception anException = new IllegalArgumentException("Bad value");
    assertThat(anException)
      .hasMessageThat()
      .startsWith("Bad");
}

5.11 Class 断言

检查类型可分配性:

@Test
public void whenCheckingClass_thenIsAssignable() {
    Class<Double> aClass = Double.class;
    assertThat(aClass).isAssignableTo(Number.class);
}

6. Java 8 类型断言

6.1 Optional 断言

核心方法:

@Test
public void whenCheckingJavaOptional_thenHasValue() {
    Optional<Integer> anOptional = Optional.of(1);
    assertThat(anOptional).hasValue(1);
}

@Test
public void whenCheckingJavaOptional_thenPresent() {
    Optional<String> anOptional = Optional.of("Baeldung");
    assertThat(anOptional).isPresent();
}

@Test
public void whenCheckingJavaOptional_thenEmpty() {
    Optional anOptional = Optional.empty();
    assertThat(anOptional).isEmpty();
}

6.2 Stream 断言

与 Iterable 断言类似:

@Test
public void whenCheckingStream_thenContainsInOrder() {
    Stream<Integer> aStream = Stream.of(1, 2, 3);
    assertThat(aStream)
      .containsAllOf(1, 2, 3)
      .inOrder();
}

更多方法参考 Iterable 断言章节。

7. Guava 类型断言

7.1 Optional 断言

与 Java 8 Optional 类似,但空值检查用 isAbsent()

@Test
public void whenCheckingGuavaOptional_thenIsAbsent() {
    Optional anOptional = Optional.absent();
    assertThat(anOptional).isAbsent();
}

7.2 Multimap 断言

支持键值对和值集合检查:

@Test
public void whenCheckingGuavaMultimap_thenExpectedSize() {
    Multimap<String, Object> aMultimap = ArrayListMultimap.create();
    aMultimap.put("one", 1L);
    aMultimap.put("one", 2.0);
    assertThat(aMultimap)
      .valuesForKey("one")
      .hasSize(2);
}

7.3 Multiset 断言

支持元素计数检查:

@Test
public void whenCheckingGuavaMultiset_thenExpectedCount() {
    TreeMultiset<String> aMultiset = TreeMultiset.create();
    aMultiset.add("baeldung", 10);
    assertThat(aMultiset).hasCount("baeldung", 10);
}

7.4 Table 断言

支持单元格和行列检查:

@Test
public void whenCheckingGuavaTable_thenContains() {
    Table<String, String, String> aTable = TreeBasedTable.create();
    aTable.put("firstRow", "firstColumn", "baeldung");
    assertThat(aTable).contains("firstRow", "firstColumn");
}

@Test
public void whenCheckingGuavaTable_thenContainsCell() {
    Table<String, String, String> aTable = TreeBasedTable.create();
    aTable.put("firstRow", "firstColumn", "baeldung");
    assertThat(aTable).containsCell("firstRow", "firstColumn", "baeldung");
}

8. 自定义失败信息与标签

Truth 默认提供清晰的失败信息,但支持自定义:

@Test
public void whenFailingAssertion_thenCustomMessage() {
    assertWithMessage("TEST-985: Secret user subject was NOT null!")
      .that(new User())
      .isNull();
}

输出:

TEST-985: Secret user subject was NOT null!:
  Not true that <com.baeldung.testing.truth.User@ae805d5e> is null

添加标签增强可读性:

@Test
public void whenFailingAssertion_thenMessagePrefix() {
    User aUser = new User();
    assertThat(aUser)
      .named("User [%s]", aUser.getName())
      .isNull();
}

输出:

Not true that User [John Doe]
  (<com.baeldung.testing.truth.User@ae805d5e>) is null

9. 扩展框架

扩展 Truth 需创建自定义 Subject 类:

  1. 继承 Subject 或其子类
  2. 定义双参数构造器(FailureStrategy + 自定义类型实例)
  3. 声明 SubjectFactory 字段
  4. 实现静态 assertThat() 方法
  5. 暴露断言 API

示例:UserSubject

public class UserSubject extends ComparableSubject<UserSubject, User> {
    private UserSubject(FailureStrategy failureStrategy, User target) {
        super(failureStrategy, target);
    }

    private static final SubjectFactory<UserSubject, User> USER_SUBJECT_FACTORY =
      new SubjectFactory<UserSubject, User>() {
        public UserSubject getSubject(FailureStrategy failureStrategy, User target) {
            return new UserSubject(failureStrategy, target);
        }
    };

    public static UserSubject assertThat(User user) {
        return Truth.assertAbout(USER_SUBJECT_FACTORY).that(user);
    }

    public void hasName(String name) {
        if (!actual().getName().equals(name)) {
            fail("has name", name);
        }
    }

    public void hasNameIgnoringCase(String name) {
        if (!actual().getName().equalsIgnoreCase(name)) {
            fail("has name ignoring case", name);
        }
    }

    public IterableSubject emails() {
        return Truth.assertThat(actual().getEmails());
    }
}

使用自定义断言:

@Test
public void whenCheckingUser_thenHasName() {
    User aUser = new User();
    assertThat(aUser).hasName("John Doe");
}

@Test
public void givenUser_whenCheckingEmails_thenExpectedSize() {
    User aUser = new User();
    assertThat(aUser)
      .emails()
      .hasSize(2);
}

10. 总结

本文系统介绍了 Truth 框架的核心能力:

  • ✅ 主流 Java/Guava 类型的断言方法
  • ✅ 失败信息定制与标签增强
  • ✅ 通过自定义 Subject 扩展框架

Truth 让测试断言更直观、失败信息更精准,是提升测试可读性的利器。完整示例代码见 GitHub


原始标题:Testing with Google Truth | Baeldung