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

单元测试已成为开发的标准部分。许多工具可以以许多不同的方式用于它。本文演示了一些提示,或者说,对我来说效果很好的最佳实践。

在本文中,您将了解
如何使用 JUnit 和 Assert 框架编写干净且可读的单元测试
在某些情况下如何避免假阳性测试
编写单元测试时应避免的事项
不要过度使用 NPE 检查
我们都倾向于在主代码中尽可能避免 NullPointerException,因为它可能会导致丑陋的后果。我相信我们主要关心的不是在测试中避免NPE。我们的目标是以干净、可读和可靠的方式验证被测组件的行为。

不良做法
过去很多次,即使不需要断言,我也使用过断言,如下例所示:isNotNull

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

此测试产生如下错误:

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

良好做法
尽管附加断言并不真正有害,但由于以下原因,应避免使用:isNotNull

它不会增加任何附加值。它只是需要阅读和维护的更多代码。
无论如何,测试都会失败,我们看到了失败的真正根本原因。该测试仍然实现了其目的。servicenull
使用 AssertJ 断言时,生成的错误消息甚至更好。
请参阅下面修改后的测试断言。

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

修改后的测试会产生如下错误:

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 中找到。

断言值而不是结果
有时,我们会编写一个正确的测试,但以“糟糕”的方式。这意味着测试完全按预期工作并验证了我们的组件,但失败没有提供足够的信息。因此,我们的目标是断言价值而不是比较结果。

不良做法
让我们看看几个这样的糟糕测试:

// #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();

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

#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 可以轻松解决问题。上面提到的所有情况都可以很容易地改写为:

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

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

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

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

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

#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 中找到。

与组相关的断言在一起
断言链接和相关的代码缩进对测试的清晰度和可读性有很大帮助。

不良做法
当我们编写测试时,我们最终会得到正确但可读性较差的测试。让我们想象一个测试,我们想要在其中查找国家/地区并进行以下检查:

计算找到的国家/地区。
使用多个值断言第一个条目。
此类测试可能如下所示:

@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

@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 中找到。

防止误报成功测试
当使用任何带有参数的断言方法时,参数也必须包含在使用者中。否则,测试将一直通过 - 即使比较失败,这意味着错误的测试。仅当断言引发 or 异常时,测试才会失败。我想这很清楚,但很容易忘记它并写错测试。它不时发生在我身上。ThrowingConsumer assertThat RuntimeException AssertionError

不良做法
假设我们有几个国家/地区代码,我们想要验证每个代码是否满足某些条件。在我们的虚拟案例中,我们想断言每个国家/地区代码都包含“a”字符。正如你所看到的,这是无稽之谈:我们有大写的代码,但我们没有在断言中应用不区分大小写。

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

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

在这里插入图片描述
良好做法
如本节开头所述,我们的测试可以通过消费者中的附加功能轻松纠正(第 7 行)。正确的测试应该是这样的:assertThat

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

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

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 fluent API 来创建更具可读性的测试。

非链接断言
让我们考虑一下 test,其目的是测试组件的日志记录。这里的目标是检查:listLogs

断言收集的日志数
断言存在和日志消息DEBU GINFO

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

链接断言
通过上面提到的 Fluent API 和链接,我们可以这样更改测试:

@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 提供了很多帮助。在本文中,提供了一些技巧和提示,以产生更清晰、更可靠的测试。请注意,这些建议大多是主观的。这取决于个人喜好和代码风格。

AssertJ 是 JAVA 的流畅断言库。示例代码:// unique entry point to get access to all assertThat methods and utility methods (e.g. entry) import static org.assertj.core.api.Assertions.*;  // common assertions assertThat(frodo.getName()).isEqualTo("Frodo"); assertThat(frodo).isNotEqualTo(sauron)                  .isIn(fellowshipOfTheRing);  // String specific assertions assertThat(frodo.getName()).startsWith("Fro")                            .endsWith("do")                            .isEqualToIgnoringCase("frodo");  // collection specific assertions assertThat(fellowshipOfTheRing).hasSize(9)                                .contains(frodo, sam)                                .doesNotContain(sauron);  // using extracting magical feature to check fellowshipOfTheRing characters name :) assertThat(fellowshipOfTheRing).extracting("name").contains("Boromir", "Gandalf", "Frodo", "Legolas")                                                   .doesNotContain("Sauron", "Elrond");  // map specific assertions, ringBearers initialized with the elves rings and the one ring bearers. assertThat(ringBearers).hasSize(4)                        .contains(entry(oneRing, frodo), entry(nenya, galadriel))                        .doesNotContainEntry(oneRing, aragorn);  // and many more assertions : dates, file, numbers, exceptions ... 标签:AssertJ
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小徐博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值