概述
本教程将介绍在外部映射中检查嵌套映射存在的方式。我们将主要讨论JUnit Jupiter API和Hamcrest API。
2. 使用Jupiter API进行断言
本文将使用JUnit 5,因此我们先来看一下Maven依赖项:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
以一个包含外层映射和内层映射的对象为例。外层映射有一个键为address
,其值是内层映射,还有一个键为name
,值为John:
:
{
"name":"John",
"address":{"city":"Chicago"}
}
通过示例,我们将断言内层映射中的键值对是否存在。
首先,我们从Jupiter API的基本方法assertTrue()
开始:
@Test
void givenNestedMap_whenUseJupiterAssertTrueWithCasting_thenTest() {
Map<String, Object> innerMap = Map.of("city", "Chicago");
Map<String, Object> outerMap = Map.of("address", innerMap);
assertTrue(outerMap.containsKey("address")
&& ((Map<String, Object>)outerMap.get("address")).get("city").equals("Chicago"));
}
我们使用布尔表达式来检查innerMap
是否存在于outerMap
中,然后验证内层映射是否有键city
,值为Chicago
。然而,由于上面使用的类型转换导致可读性降低。让我们尝试修复它:
@Test
void givenNestedMap_whenUseJupiterAssertTrueWithoutCasting_thenTest() {
Map<String, Object> innerMap = Map.of("city", "Chicago");
Map<String, Map<String, Object>> outerMap = Map.of("address", innerMap);
assertTrue(outerMap.containsKey("address") && outerMap.get("address").get("city").equals("Chicago"));
}
我们改变了之前声明外层映射的方式,将其定义为Map<String, Map<String, Object>>
而非Map<String, Object>
。这样避免了类型转换,代码稍微更易读。
但若测试失败,我们无法确切知道哪个断言失败。为了解决这个问题,我们可以引入assertAll()
方法:
@Test
void givenNestedMap_whenUseJupiterAssertAllAndAssertTrue_thenTest() {
Map<String, Object> innerMap = Map.of("city", "Chicago");
Map<String, Map<String, Object>> outerMap = Map.of("address", innerMap);
assertAll(
() -> assertTrue(outerMap.containsKey("address")),
() -> assertEquals(outerMap.get("address").get("city"), "Chicago")
);
}
我们将布尔表达式移动到了assertAll()
中的assertTrue()
和assertEquals()
方法中,这样就能明确知道哪部分失败。此外,也提高了可读性。
3. 使用Hamcrest API进行断言
Hamcrest库提供了一个灵活的框架,借助Matchers编写JUnit测试。我们将利用其内置的Matchers,以及其框架开发自定义Matcher,来验证嵌套映射中的键值对存在。
3.1. 使用现成的Matchers
为了使用Hamcrest库,我们需要更新pom.xml
文件中的Maven依赖项:
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
在深入示例之前,先了解一下Hamcrest库中对Map
的测试支持。Hamcrest提供了以下几种与assertThat()
方法配合使用的Matchers:
-
hasEntry()
- 创建一个匹配器,当检查的Map
至少包含一个键值对,其中键等于指定键,值等于指定值时匹配。 -
hasKey()
- 创建一个匹配器,当检查的Map
至少包含一个满足指定匹配器的键时匹配。 -
hasValue()
- 创建一个匹配器,当检查的Map
至少包含一个满足指定值匹配器的值时匹配。
让我们从基本示例开始,但使用assertThat()
和hasEntry()
方法:
@Test
void givenNestedMap_whenUseHamcrestAssertThatWithCasting_thenTest() {
Map<String, Object> innerMap = Map.of("city", "Chicago");
Map<String, Object> outerMap = Map.of("address", innerMap);
assertThat((Map<String, Object>)outerMap.get("address"), hasEntry("city", "Chicago"));
}
除了丑陋的类型转换,测试读起来更容易理解。不过,我们漏掉了在获取内层映射值之前检查外层映射是否有address
键。
我们能否修复上述测试?可以使用hasKey()
和hasEntry()
来断言内层映射的存在:
@Test
void givenNestedMap_whenUseHamcrestAssertThat_thenTest() {
Map<String, Object> innerMap = Map.of("city", "Chicago");
Map<String, Map<String, Object>> outerMap = Map.of("address", innerMap);
assertAll(
() -> assertThat(outerMap, hasKey("address")),
() -> assertThat(outerMap.get("address"), hasEntry("city", "Chicago"))
);
}
有趣的是,我们在Jupiter库和Hamcrest库之间结合了assertAll()
方法来测试映射。同时,我们调整了变量outerMap
的定义以消除类型转换。
仅使用Hamcrest库,我们还可以这样做:
@Test
void givenNestedMapOfStringAndObject_whenUseHamcrestAssertThat_thenTest() {
Map<String, Object> innerMap = Map.of("city", "Chicago");
Map<String, Map<String, Object>> outerMap = Map.of("address", innerMap);
assertThat(outerMap, hasEntry(equalTo("address"), hasEntry("city", "Chicago")));
}
令人惊讶的是,我们可以嵌套hasEntry()
方法。这得益于equalTo()
方法的帮助。没有它,方法可以编译,但断言会失败。
3.2. 使用自定义Matcher
到目前为止,我们尝试了现有的方法来检查嵌套映射。现在,让我们尝试通过扩展Hamcrest库中的TypeSafeMatcher
类创建自定义Matcher。
public class NestedMapMatcher<K, V> extends TypeSafeMatcher<Map<K, Object>> {
private K key;
private V subMapValue;
public NestedMapMatcher(K key, V subMapValue) {
this.key = key;
this.subMapValue = subMapValue;
}
@Override
protected boolean matchesSafely(Map<K, Object> item) {
if (item.containsKey(key)) {
Object actualValue = item.get(key);
return subMapValue.equals(actualValue);
}
return false;
}
@Override
public void describeTo(Description description) {
description.appendText("a map containing key ").appendValue(key)
.appendText(" with value ").appendValue(subMapValue);
}
public static <K, V> Matcher<V> hasNestedMapEntry(K key, V expectedValue) {
return new NestedMapMatcher(key, expectedValue);
}
}
我们重写了matchesSafely()
方法来检查嵌套映射。
看下如何使用它:
@Test
void givenNestedMapOfStringAndObject_whenUseHamcrestAssertThatAndCustomMatcher_thenTest() {
Map<String, Object> innerMap = Map.of
(
"city", "Chicago",
"zip", "10005"
);
Map<String, Map<String, Object>> outerMap = Map.of("address", innerMap);
assertThat(outerMap, hasNestedMapEntry("address", innerMap));
}
在assertThat()
方法中检查嵌套映射的表达式明显简化了。我们只需调用hasNestedMapEntry()
方法检查innerMap
。此外,它会检查整个内层映射,不像之前的检查只检查一个条目。
有趣的是,即使将outerMap
定义为Map(String, Object)
,自定义的Matcher
也能工作。我们无需进行任何类型转换。
@Test
void givenOuterMapOfStringAndObjectAndInnerMap_whenUseHamcrestAssertThatAndCustomMatcher_thenTest() {
Map<String, Object> innerMap = Map.of
(
"city", "Chicago",
"zip", "10005"
);
Map<String, Object> outerMap = Map.of("address", innerMap);
assertThat(outerMap, hasNestedMapEntry("address", innerMap));
}
4. 结论
在这篇文章中,我们讨论了在内嵌映射中检查键值对存在的不同方法。我们探索了JUnit的Jupiter API和Hamcrest API。
Hamcrest提供了出色的现成方法来支持对嵌套映射的断言,减少了样板代码,使测试更加声明式。尽管如此,我们仍需要编写自定义Matcher,以使断言更直观,并支持在嵌套映射中检查多个条目。
如往常一样,本文中的代码可以在GitHub上找到。