当前位置: 首页 > 工具软件 > AssertJ > 使用案例 >

使用AssertJ让单元测试和TDD更加简单

訾安邦
2023-12-01

使用AssertJ让单元测试和TDD更加简单

前言

在编写单元测试时,一个重要的工作就是编写断言(Assertion),而JUnit自带的断言机制和Hamcrest的assertThat都不那么好用。

利用AssertJ,可以让单元测试更加简单,让TDD过程更加顺畅。

AssertJ的优点:

  • 通用的assertThat “流式断言”,让编写断言更加简单快捷。
  • API丰富,对各种类型的断言都非常友好。

环境

本文的测试环境:

  • JUnit4
  • assertj-core
  • Maven
  • Java8

添加assertJ的Maven依赖:

<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <version>3.17.0</version>
  <scope>test</scope>
</dependency>

在测试类中引入AssertJ:

import static org.assertj.core.api.Assertions.*;

断言类型

常用的断言类型

常用的断言类型包括:

  • isSameAs: 同一个对象。
  • isEuqalsTo: 值相等。
  • isCloseTo: 值相近。
  • isTrue:为真,isFalse 为假。
  • containsExactly:包含全部元素,顺序也保持一致。
  • containsOnly:包含全部元素,顺序不需要保持一致。
  • contains:包含给定的元素。
  • hasSize:长度或大小或元素个数为N个。
  • isEmpty: 是否为空。
  • isNull:是否为null。
  • isInstanceOf:是否为指定类型。

断言的辅助方法

断言的辅助方法:

  • extracting: 提取,根据类名或方法引用或字段名反射调用,或根据lambda表达式调用。

  • mathes: 匹配,根据lambda表达式调用。

  • atIndex: 获取指定位置/索引的元素。

  • offset: 偏移量。

  • withPercentage:百分比。

  • tuple:将一组属性的值包装成元组。

  • entry:将键值对({K,V})包装成Map.Entry。

AssertJ例子

官网:

基本类型

参见:

说明:

  • 对可以精确匹配的值(比如字符串、整数),用isEqualTo()来比较,对不能精确匹配的值(比如浮点数),用isCloseTo()比较。
  • 对布尔值,用isTrue()isFalse()来比较。
@Test
public void test_value_equals() {
  String hello = "hello".toUpperCase();
  assertThat(hello).isEqualTo("HELLO");

  int secondsOfDay = 24 * 60 * 60;
  assertThat(secondsOfDay).isEqualTo(86400);
}

@Test
public void test_value_close() {
  double result = 0.1 + 0.1 + 0.1; // 0.30000000000000004
  assertThat(result).isCloseTo(0.3, offset(0.0001)); // 误差值
  assertThat(result).isCloseTo(0.3, withPercentage(0.01)); // 误差百分比
}

@Test
public void test_boolean() {
  boolean flag = "Kubernetes".length() > 8;
  assertThat(flag).isTrue();

  boolean flag2 = "Docker".length() > 8;
  assertThat(flag2).isFalse();
}

单个对象

参见:

说明:

  • 判断是否为同一个对象用isSameAs()
  • 如果重写了euqals和hashcode方法,也可以用isEqualTo来判断对象是否相同。
  • 如果只是判断对象的值是否相等,则可以用extracting提取后再判断,或用matches来用lambda表达式判断。
  • 判断是否为null用isNull()isNotNull()
@Test
public void test_object_null_or_not_null() {
    Person p1 = new Person("William", 34);
    assertThat(p1).isNotNull();

    Person p2 = null;
    assertThat(p2).isNull();
}

@Test
public void test_object_same_as_other() {
    Person p1 = new Person("William", 34);
    Person p2 = p1;
    assertThat(p1).isSameAs(p2);

    Person p3 = new Person("John", 35);
    assertThat(p1).isNotSameAs(p3);
}

@Test
public void test_object_equals() {
    Person p1 = new Person("William", 34);
    Person p2 = new Person("William", 34);

    assertThat(p1).isNotSameAs(p2);
    assertThat(p1).isNotEqualTo(p2); // 如果用isEqualTo判断,则必须要重写equals方法

    // extracting method reference
    assertThat(p1).extracting(Person::getName, Person::getAge).containsExactly("William", 34);
    assertThat(p1).extracting(Person::getName, Person::getAge).containsExactly(p2.getName(), p2.getAge());

    // extracting field
    assertThat(p1).extracting("name", "age").containsExactly("William", 34);
    assertThat(p1).extracting("name", "age").containsExactly(p2.getName(), p2.getAge());

    // matches
    assertThat(p1).matches(x -> x.getName().equals("William") && x.getAge() == 34);
    assertThat(p1).matches(x -> x.getName().equals(p2.getName()) && x.getAge() == p2.getAge());
}

数组

参见:

说明:

  • isNull来判断数组是否为null。
  • isEmpty来判断数组是否为空(不包含任何元素)。
  • hasSize来判断数组的元素个数。
  • contains 判断数组中包含指定元素;用containsOnly判断数组中包含全部元素,但是顺序可以不一致;用cotainsExactly判断数组中包含全部元素且顺序需要一致。
  • 如果数组中的元素为对象,则需要通过extracting提取出对象的属性值,再来判断;如果提取出对象的多个属性值时,可以用tuple将多个属性值包装成元组
  • atIndex来获取指定位置/索引的元素
@Test
public void test_array_null_or_empty() {
    String[] nullNames = null;
    assertThat(nullNames).isNull();

    String[] emptyNames = {};
    assertThat(emptyNames).isEmpty();
    assertThat(emptyNames).hasSize(0);
}

@Test
public void test_array_contains() {
    String[] names = {"Python", "Golang", "Docker", "Java"};

    assertThat(names).contains("Docker");
    assertThat(names).doesNotContain("Haddop");

    assertThat(names).containsExactly("Python", "Golang", "Docker", "Java"); // 完全匹配,且顺序也一致
    assertThat(names).contains("Java", "Docker", "Golang", "Python"); // 完全匹配,顺序可以不一致

    assertThat(names).contains("Docker", atIndex(2)); // names[2]
}

@Test
public void test_array_object_contains() {
    Person[] names = {new Person("William", 34),
                      new Person("John", 36),
                      new Person("Tommy", 28),
                      new Person("Lily", 32)};

    assertThat(names).extracting(Person::getName)
      .containsExactly("William", "John", "Tommy", "Lily");

    assertThat(names).extracting("name", "age")
      .containsExactly(tuple("William", 34),
                       tuple("John", 36),
                       tuple("Tommy", 28),
                       tuple("Lily", 32));

    assertThat(names).extracting(x -> x.getName(), x -> x.getAge())
      .containsExactly(tuple("William", 34),
                       tuple("John", 36),
                       tuple("Tommy", 28),
                       tuple("Lily", 32));
}

集合

List

参见:

List的断言与数组的断言类似。

@Test
public void test_list_contains() {
    List<Person> names = Arrays.asList(new Person("William", 34),
                                       new Person("John", 36),
                                       new Person("Tommy", 28),
                                       new Person("Lily", 32));

    assertThat(names).extracting(Person::getName)
      .containsExactly("William", "John", "Tommy", "Lily");

    assertThat(names).extracting("name", "age")
      .containsExactly(tuple("William", 34),
                       tuple("John", 36),
                       tuple("Tommy", 28),
                       tuple("Lily", 32));

    assertThat(names).extracting(x -> x.getName(), x -> x.getAge())
      .containsExactly(tuple("William", 34),
                       tuple("John", 36),
                       tuple("Tommy", 28),
                       tuple("Lily", 32));
  }
}

Map

参见:

说明:

  • 可以对key进行断言:

    • containsKeys:包含指定key。
    • containsOnlyKeys:包含全部key,对顺序无要求。
  • 可以对value进行断言:

    • containsValues:包含指定value。
  • 可以对entry进行断言:

    • contains:包含指定entry。
    • containsOnly:包含全部entry,对顺序无要求。
  • 注意,HashMap中的entry无序,而TreeMap中的entry有序。因此TreeMap可用containsExactly判断是否包含全部Entry,且顺序也保持一致。

@Test
public void test_hash_map_contains() {
    Person william = new Person("William", 34);
    Person john = new Person("John", 36);
    Person tommy = new Person("Tommy", 28);
    Person lily = new Person("Lily", 32);
    Person jimmy = new Person("Jimmy", 38);


    Map<String, Person> map = new HashMap<>();
    map.put("A1001", william);
    map.put("A1002", john);
    map.put("A1003", tommy);
    map.put("A1004", lily);

    Map<String, Person> map2 = new HashMap<>();
    map2.put("A1001", william);
    map2.put("A1002", john);
    map2.put("A1003", tommy);
    map2.put("A1004", lily);

    // contains keys
    assertThat(map).containsKeys("A1003");
    assertThat(map).containsOnlyKeys("A1001", "A1002", "A1003", "A1004"); // 需要包含全部的key
    assertThat(map).doesNotContainKeys("B1001");

    // contains values
    assertThat(map).containsValues(tommy);
    assertThat(map).doesNotContainValue(jimmy);

    // contains entries
    assertThat(map).containsEntry("A1003", tommy);
    assertThat(map).containsAllEntriesOf(map2);
    assertThat(map).contains(entry("A1003", tommy));
    // 需要包含全部的entry
    assertThat(map).containsOnly(entry("A1001", william),
                                 entry("A1002", john),
                                 entry("A1003", tommy),
                                 entry("A1004", lily));

}

@Test
public void test_tree_map_contains() {
    Person william = new Person("William", 34);
    Person john = new Person("John", 36);
    Person tommy = new Person("Tommy", 28);
    Person lily = new Person("Lily", 32);


    Map<String, Person> map = new TreeMap<>();
    map.put("A1001", william);
    map.put("A1002", john);
    map.put("A1003", tommy);
    map.put("A1004", lily);

    // 需要包含全部的entry,且顺序也一致
    assertThat(map).containsExactly(entry("A1001", william),
                                    entry("A1002", john),
                                    entry("A1003", tommy),
                                    entry("A1004", lily));

}

@Test
public void test_map_extracting() {
    Person william = new Person("William", 34);
    Person john = new Person("John", 36);
    Person tommy = new Person("Tommy", 28);
    Person lily = new Person("Lily", 32);

    Map<String, Person> map = new TreeMap<>();
    map.put("A1001", william);
    map.put("A1002", john);
    map.put("A1003", tommy);
    map.put("A1004", lily);

    assertThat(map).extracting("A1001").isEqualTo(william);
    assertThat(map).extracting("A1001")
      .extracting("name", "age")
      .containsExactly("William", 34);
}

Set

Set的断言与List的断言类似。

异常

参见:

说明:

  • try catch 来捕捉可能抛出的异常。
  • isInstanceOf来判断异常类型。
  • hasMessageContaining来模糊匹配异常信息,用hasMessage来精确匹配异常信息。
  • hasCauseInstanceOf 来判断异常原因(root cause)的类型。
  • 对checked exception和runtime exception的断言是一样的。
public class ExceptionExampleTest {


    public String readFirstLine(String fileName) throws IncorrectFileNameException {
        try (Scanner file = new Scanner(new File(fileName))) {
            if (file.hasNextLine()) {
                return file.nextLine();
            }
        } catch (FileNotFoundException e) {
           throw new IncorrectFileNameException("Incorrect file name: " + fileName, e);
        }
        return "";
    }

    /**
     * java custom exception:
     * https://www.baeldung.com/java-new-custom-exception
      */

    public class IncorrectFileNameException extends Exception {
        public IncorrectFileNameException(String message) {
            super(message);
        }

        public IncorrectFileNameException(String message, Throwable cause) {
            super(message, cause);
        }
    }


    @Test
    public void test_exception_ArithmeticException() {
        try {
            double result = 1 / 0;
        } catch(Exception e) {
            // check exception type
            assertThat(e).isInstanceOf(ArithmeticException.class);

            // check exception message
            assertThat(e).hasMessageContaining("/ by zero");
        }
    }

    @Test
    public void test_exception_ArrayIndexOutOfBoundsException() {
        try {
            String[] names = {"William", "John", "Tommy", "Lily"};
            String name = names[4];
        } catch (Exception e) {
            // check exception type
            assertThat(e).isInstanceOf(ArrayIndexOutOfBoundsException.class);

            // check exception message
            assertThat(e).hasMessage("4");
        }
    }

    @Test
    public void test_exception_IllegalStateException() {
        try {
            List<String> names = Arrays.asList("William", "John", "Tommy", "Lily");
            Iterator<String> iter = names.iterator();
            iter.remove();
        } catch (Exception e) {
            // check exception type
            assertThat(e).isInstanceOf(IllegalStateException.class);
        }
    }


    @Test
    public void test_custom_exception() {
        try {
            String line = readFirstLine("./unknown.txt");
        } catch (Exception e) {
            // check exception type
            assertThat(e).isInstanceOf(IncorrectFileNameException.class);

            // check exception message
            assertThat(e).hasMessageContaining("Incorrect file name");

            // check cause type
            assertThat(e).hasCauseInstanceOf(FileNotFoundException.class);
        }
    }
}

Optional

参见:

说明:

  • isPresent来判断Optional是否为空。
  • 如果Optional中的值是基本类型,用hasValue判断。
  • 如果Optional中的值为对象,用hasValueSatisfying判断。
public class OptionalExampleTest {
    
    @Test
    public void test_optional_null_or_empty() {
        Optional<String> op1 = Optional.ofNullable(null);

        assertThat(op1).isNotPresent();
        assertThat(op1).isEmpty();

        Optional<String> op2 = Optional.empty();
        assertThat(op2).isNotPresent();
        assertThat(op2).isEmpty();
    }

    @Test
    public void test_optional_basic_type() {
        Optional<String> op1 = Optional.ofNullable("hello");
        assertThat(op1).isPresent().hasValue("hello");

        Optional<Integer> op2 = Optional.ofNullable(365);
        assertThat(op2).isPresent().hasValue(365);
    }

    @Test
    public void test_optional_object() {
        Optional<Person> p1 = Optional.ofNullable(new Person("William", 34));

        assertThat(p1).isPresent()
                .hasValueSatisfying(x -> {
                    assertThat(x).extracting(Person::getName, Person::getAge)
                            .containsExactly("William", 34);
                });
    }

}
 类似资料: