概述

本教程将介绍在外部映射中检查嵌套映射存在的方式。我们将主要讨论JUnit Jupiter APIHamcrest 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上找到。