Contents

使用 Google Truth 进行测试

1. 概述

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

在本文中,我们将探索Truth框架的关键特性并实施示例来展示其功能。

2. Maven依赖

首先,我们需要将truthtruth-java8-extension添加到我们的pom.xml 中:

<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 上找到最新版本的truthtruth-java8-extension

3. 简介

Truth允许我们为各种类编写可读的断言和失败消息:

  • 标准 Java——原语、数组、字符串、对象、集合、可抛出对象、类等。
  • Java 8——OptionalStream实例
  • Guava——OptionalMultimapMultisetTable对象
  • 自定义类型——通过扩展Subject类,我们稍后会看到

通过TruthTruth8类,该库提供了用于编写适用于subject的断言的实用方法,即被测值或对象。

一旦知道了主题,Truth就可以在编译时推断该主题已知的命题。这允许它返回围绕我们的值的包装器,这些包装器声明特定于该特定主题的命题方法。

例如,在对列表进行断言时,Truth返回一个IterableSubject实例,该实例定义了contains()containsAnyOf()等方法。在Map上断言时,它返回一个MapSubject ,该 MapSubject声明了*containsEntry()containsKey()*等方法。

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("contact@blogdemo.com", "staff@blogdemo.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);
    }
    // standard constructors, getters and setters
}

请注意自定义的equals()方法,其中我们声明两个User对象如果它们的名称相等,则它们是相等的。

5. 标准 Java 断言

在本节中,我们将看到如何为标准 Java 类型编写测试断言的详细示例。

5.1. Subject断言

Truth提供了Subject包装器,用于对对象执行断言。Subject也是库中所有其他包装器的父级,并声明了用于确定Object(在我们的例子中为User)是否等于另一个对象的方法:

@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() {
    // ...
    assertThat(aUser).isNotIn(Arrays.asList(1, 3, "Three"));
}

是否为空:

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

或者如果它是特定类的实例:

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

Subject类中还有其他断言方法。要全部发现它们,请参阅Subject文档

在接下来的部分中,我们将重点关注 Truth支持的每种特定类型最相关的方法。但是,请记住,也可以应用Subject类中的所有方法。

5.2. IntegerFloatDouble断言

可以比较IntegerFloatDouble实例是否相等:

@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.0f;
    assertThat(aDouble).isLessThan(20.0);
}

此外,还可以检查FloatDouble实例以查看它们是否在预期的精度内:

@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断言

除了常见的断言之外,这种类型可以忽略其规模进行比较:

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

5.4. boolean断言

只提供了两个相关的方法,isTrue()isFalse()

@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");
}

此外,我们可以检查字符串是否包含给定的字符串,是否以预期值结尾或是否为空。源代码中提供了这些方法和其他方法的测试用例。

5.6. 数组断言

我们可以检查Array以查看它们是否等于其他数组:

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

或者如果它们是空的:

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

5.7. Comparable断言

除了测试Comparable是否大于或小于另一个实例之外,我们还可以检查它们是否至少是给定值:

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

此外,我们可以测试它们是否在特定范围内:

@Test
public void whenCheckingComparable_thenInRange() {
    // ...
    assertThat(aComparable).isIn(Range.closed(1, 10));
}

或在特定列表中:

@Test
public void whenCheckingComparable_thenInList() {
    // ...
    assertThat(aComparable).isIn(Arrays.asList(4, 5, 6));
}

我们还可以根据类的compareTo()方法测试两个Comparable实例是否等价。

首先,让我们修改我们的User类来实现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断言

除了断言Iterable实例的大小,无论它是空的还是没有重复的,Iterable上最典型的断言是它包含一些元素:

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

它包含另一个Iterable的任何元素:

@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断言

除了断言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() {
    // ...
    assertThat(map).containsKey("one");
}

或者如果它与另一个Map具有相同的条目:

@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断言

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断言

Class断言只有一个重要的方法,我们可以用它来测试一个类是否可以分配给另一个类:

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

6. Java 8 断言

OptionalStreamTruth支持的仅有的两种 Java 8 类型。

6.1. Optional断言

有三种重要的方法来验证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("Blogdemo");
    assertThat(anOptional).isPresent();
}

或者如果该值不存在:

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

6.2. Stream断言

Stream的断言与Iterable的断言非常相似。

例如,我们可以测试一个特定的Stream是否以相同的顺序包含一个Iterable的所有对象:

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

有关更多示例,请参阅可Iterable断言部分。

7. Guava 类型断言

在本节中,我们将看到Truth中支持的 Guava 类型的断言示例。

7.1. Optional断言

Guava Optional还有三个重要的断言方法。*hasValue()isPresent()*方法的行为与 Java 8 Optional完全相同。

但是我们使用isAbsent()代替isEmpty()断言Optional不存在:

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

7.2. Multimap断言

Multimap和标准Map断言非常相似。

一个显着的区别是我们可以在Multimap中获取键的多个值并对这些值进行断言。

这是一个测试“one”键的值是否为 2 的示例:

@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);
}

有关更多示例,请参阅Map断言部分。

7.3. Multiset断言

Multiset对象的断言包括 Iterable 的断言和一个额外的方法来验证键是否具有特定的出现次数:

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

7.4. Table断言

除了检查它的大小或它是空的,我们可以检查一个Table来验证它是否包含给定行和列的特定映射:

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

或者如果它包含特定的单元格:

@Test
public void whenCheckingGuavaTable_thenContainsCell() {
    Table<String, String, String> aTable = getDummyGuavaTable();
    assertThat(aTable).containsCell("firstRow", "firstColumn", "blogdemo");
}

此外,我们可以检查它是否包含给定的行、列或值。请参阅相关测试用例的源代码。

8. 自定义失败消息和标签

当一个断言失败时,Truth会显示非常易读的消息,指出到底出了什么问题。但是,有时需要向这些消息添加更多信息以提供有关所发生事件的更多详细信息。

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.blogdemo.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.blogdemo.testing.truth.User@ae805d5e>) is null

9. 扩展

扩展Truth意味着我们可以添加对自定义类型的支持。为此,我们需要创建一个类:

  • 扩展Subject类或其子类之一
  • 定义一个接受两个参数的构造函数——一个FailureStrategy和一个我们自定义类型的实例
  • 声明一个SubjectFactory类型的字段,Truth将使用它来创建我们自定义主题的实例
  • 实现一个接受我们自定义类型的静态*assertThat()*方法
  • 公开我们的测试断言 API

现在我们知道了如何扩展Truth,让我们创建一个类来添加对User类型对象的支持:

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());
    }
}

现在,我们可以静态导入自定义主题的*assertThat()*方法并编写一些测试:

@Test
public void whenCheckingUser_thenHasName() {
    User aUser = new User();
    assertThat(aUser).hasName("John Doe");
}
@Test
public void whenCheckingUser_thenHasNameIgnoringCase() {
    // ...
    assertThat(aUser).hasNameIgnoringCase("john doe");
}
@Test
public void givenUser_whenCheckingEmails_thenExpectedSize() {
    // ...
    assertThat(aUser)
      .emails()
      .hasSize(2);
}