文章目录
使用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例子
官网:
- https://github.com/assertj/assertj-core
- https://github.com/joel-costigliola/assertj-examples/tree/main/assertions-examples/src/test/java/org/assertj/examples
基本类型
参见:
说明:
-
对可以精确匹配的值(比如字符串、整数),用
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); }); }
}