使用AssertJ进行单元测试的提示

单元测试已成为开发的标准组成部分。有多种工具可以以不同方式用于单元测试。本文将展示一些对我有效的提示或最佳实践。

在本文中,你将学习

不要过度使用NPE检查

我们都倾向于尽可能避免NullPointerException,因为在主代码中它可能导致不良后果。我相信我们的主要关注点不是在测试中避免NPE。我们的目标是验证被测试组件的行为,使其清晰、易读且可靠。

不良实践

过去我多次使用isNotNull断言,即使它并不必要,如下例所示:

Java

 

@Test
public void getMessage() {
	assertThat(service).isNotNull();
	assertThat(service.getMessage()).isEqualTo("Hello world!");
}

此测试会产生如下错误:

Plain Text

 

java.lang.AssertionError: 
Expecting actual not to be null
	at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)

良好实践

尽管额外的isNotNull断言并无实际害处,但应避免使用,原因如下:

  • 它不增加任何额外价值。它只是增加了阅读和维护的代码量。
  • servicenull时,测试无论如何都会失败,并且我们看到了失败的真正根源。测试仍然达到了其目的。
  • 使用AssertJ断言产生的错误信息更为完善。

请看下面的修改后的测试断言。

Java

 

@Test
public void getMessage() {
	assertThat(service.getMessage()).isEqualTo("Hello world!");
}

修改后的测试会产生这样的错误:

Java

 

java.lang.NullPointerException: Cannot invoke "com.github.aha.poc.junit.spring.HelloService.getMessage()" because "this.service" is null
	at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)

注意:该示例可在SimpleSpringTest中找到。

断言值而非结果

我们有时会编写一个正确的测试,但方式不佳。这意味着测试完全按照预期工作并验证了我们的组件,但失败信息并不充分。因此,我们的目标是断言值,而不是比较结果。

不良做法

以下是几个这样的不良测试示例:

Java

 

// #1
assertThat(argument.contains("o")).isTrue();

// #2
var result = "Welcome to JDK 10";
assertThat(result instanceof String).isTrue();

// #3
assertThat("".isBlank()).isTrue();

// #4
Optional<Method> testMethod = testInfo.getTestMethod();
assertThat(testMethod.isPresent()).isTrue();

上述测试中的一些错误如下所示。

Plain Text

 

#1
Expecting value to be true but was false
	at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
	at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
  
#3
Expecting value to be true but was false
	at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
	at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)

良好做法

使用AssertJ及其流畅的API解决方案相当简单。上述所有情况都可以轻松重写为:

Java

 

// #1
assertThat(argument).contains("o");

// #2
assertThat(result).isInstanceOf(String.class);

// #3
assertThat("").isBlank();

// #4
assertThat(testMethod).isPresent();

与之前提到的相同错误现在提供了更多价值。

Plain Text

 

#1
Expecting actual:
  "Hello"
to contain:
  "f" 
	at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
    
#3
Expecting blank but was: "a"
	at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)

注意:示例可在SimpleParamTests中找到。

将相关断言分组

断言链和相关的代码缩进极大地有助于测试的清晰度和可读性。

不良实践

在编写测试时,我们可能会得到正确但可读性较差的测试。假设我们想要查找国家并进行以下检查:

  1. 统计找到的国家数量。
  2. 断言第一个条目与多个值。

此类测试可能如下所示:

Java

 

@Test
void listCountries() {
	List<Country> result = ...;

	assertThat(result).hasSize(5);
	var country = result.get(0);
	assertThat(country.getName()).isEqualTo("Spain");
	assertThat(country.getCities().stream().map(City::getName)).contains("Barcelona");
}

良好实践

尽管之前的测试是正确的,但我们应该通过将相关断言分组在一起(第9-11行)来大幅提高可读性。这里的目的是对result进行一次断言,并根据需要编写多个链式断言。请参阅下面的修改版本。

Java

 

@Test
void listCountries() {
	List<Country> result = ...;

	assertThat(result)
		.hasSize(5)
		.singleElement()
		.satisfies(c -> {
			assertThat(c.getName()).isEqualTo("Spain");
			assertThat(c.getCities().stream().map(City::getName)).contains("Barcelona");
		});
}

注意:示例可在CountryRepositoryOtherTests中找到。

防止假阳性成功测试

当使用任何带有ThrowingConsumer参数的断言方法时,该参数内部也必须包含assertThat。否则,测试将始终通过——即使在比较失败时也是如此,这意味着测试错误。只有当断言抛出RuntimeExceptionAssertionError异常时,测试才会失败。我想这很清楚,但很容易忘记这一点并编写错误的测试。我偶尔也会犯这样的错误。

不当实践

假设我们有一系列国家代码,并希望验证每个代码是否满足某些条件。在我们的虚构案例中,我们要断言每个国家代码包含字符”a”。如您所见,这是无意义的:我们的代码是大写的,但在断言中并未应用大小写不敏感。

Java

 

@Test
void assertValues() throws Exception {
	var countryCodes = List.of("CZ", "AT", "CA");
	
	assertThat( countryCodes )
		.hasSize(3)
		.allSatisfy(countryCode -> countryCode.contains("a"));
}

令人惊讶的是,我们的测试成功通过了。

正确实践

如本节开头所述,通过在消费者中添加额外的assertThat(第7行),我们的测试可以轻松修正。正确的测试应如下所示:

Java

 

@Test
void assertValues() throws Exception {
	var countryCodes = List.of("CZ", "AT", "CA");
	
	assertThat( countryCodes )
		.hasSize(3)
		.allSatisfy(countryCode -> assertThat( countryCode ).containsIgnoringCase("a"));
}

现在,测试按预期失败,并显示正确的错误消息。

Plain Text

 

java.lang.AssertionError: 
Expecting all elements of:
  ["CZ", "AT", "CA"]
to satisfy given requirements, but these elements did not:

"CZ"
error: 
Expecting actual:
  "CZ"
to contain:
  "a"
 (ignoring case)
	at com.github.aha.sat.core.clr.AppleTest.assertValues(AppleTest.java:45)

链式断言

最后一个提示并非实践,而更像是建议。应利用AssertJ的流畅API来创建更具可读性的测试。

非链式断言

考虑listLogs测试,其目的是测试组件的日志记录。这里的目的是检查:

  • 声明收集的日志数量
  • 断言存在DEBUGINFO日志消息
Java

 

@Test
void listLogs() throws Exception {
	ListAppender<ILoggingEvent> logAppender = ...;
	
	assertThat( logAppender.list ).hasSize(2);
	assertThat( logAppender.list ).anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
			assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
		});
	assertThat( logAppender.list ).anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(INFO);
			assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
		});
}

链接断言

通过提及的流式API及链接,我们可以这样修改测试:

Java

 

@Test
void listLogs() throws Exception {
	ListAppender<ILoggingEvent> logAppender = ...;
	
	assertThat( logAppender.list )
		.hasSize(2)
		.anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
			assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
		})
		.anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(INFO);
			assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
		});
}

注意:示例可在AppleTest中找到。

总结与源码

AssertJ框架通过其流式API提供了大量帮助。本文中,为了编写更清晰、更可靠的测试,提供了若干技巧和提示。请注意,这些建议大多具有主观性,取决于个人偏好和代码风格。

所用源码可在我的仓库中找到:

Source:
https://dzone.com/articles/hints-for-unit-testing-with-assertj