在编写单元测试时,一个重要的工作就是编写断言(Assertion),而JUnit自带的断言机制和Hamcrest的assertThat
都不那么好用。
利用AssertJ,可以让单元测试更加简单,让TDD过程更加顺畅。
AssertJ的优点:
assertThat
“流式断言”,让编写断言更加简单快捷。本文的测试环境:
添加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。
官网:
参见:
说明:
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()
。isEqualTo
来判断对象是否相同。extracting
提取后再判断,或用matches
来用lambda表达式判断。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的断言与数组的断言类似。
@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));
}
}
参见:
说明:
可以对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的断言与List的断言类似。
参见:
说明:
try catch
来捕捉可能抛出的异常。isInstanceOf
来判断异常类型。hasMessageContaining
来模糊匹配异常信息,用hasMessage
来精确匹配异常信息。hasCauseInstanceOf
来判断异常原因(root cause)的类型。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);
}
}
}
参见:
说明:
isPresent
来判断Optional是否为空。hasValue
判断。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);
});
}
}