How to assert Map contains Map with entry
Asked Answered
R

4

16

I have a unit test that needs to check for a nested map value. I can get my assertion to work by pulling out the entry and matching the underlying Map, but I was looking for a clear way to show what the assertion is doing. Here is a very simplified test:

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasEntry;

import java.util.HashMap;
import java.util.Map;

import org.junit.Test;

public class MapContainsMapTest {
    @Test
    public void testMapHasMap() {
        Map<String, Object> outerMap = new HashMap<String, Object>();
        Map<String, Object> nestedMap = new HashMap<String, Object>();
        nestedMap.put("foo", "bar");
        outerMap.put("nested", nestedMap);

        // works but murky
        assertThat((Map<String, Object>) outerMap.get("nested"), hasEntry("foo", "bar"));
        // fails but clear
        assertThat(outerMap, hasEntry("nested", hasEntry("foo", "bar")));
    }
}

It seems the problem is the outer map is being compared using hasEntry(K key, V value) while what I want to use is hasEntry(Matcher<? super K> keyMatcher, Matcher<? super V> valueMatcher). I am not sure how to coerce the assertion to use the second form.

Thanks in advance.

Roundlet answered 20/7, 2015 at 18:31 Comment(2)
"It seems the problem is the outer map is being compared using ..." hasEntry(is("key"), is("value")) for example would cause the second form to be used. Maybe you could use equalTo matcher in place of is, but either way, the readability goes out of the window.Ambagious
The problem is with the outer test, so I need something like equalTo("nested")... but that doesn't work unless I change the outer map type to Map<String, Map<String,Object>>Roundlet
S
4

I would probably extend a new Matcher for that, something like that (beware, NPEs lurking):

class SubMapMatcher extends BaseMatcher<Map<?,?>> {

private Object key;
private Object subMapKey;
private Object subMapValue;

public SubMapMatcher(Object key, Object subMapKey, Object subMapValue) {
    super();
    this.key = key;
    this.subMapKey = subMapKey;
    this.subMapValue = subMapValue;
}

@Override
public boolean matches(Object item) {

    Map<?,?> map = (Map<?,?>)item;

    if (!map.containsKey(key)) {
        return false;
    }

    Object o = map.get(key);

    if (!(o instanceof Map<?,?>)) {
        return false;
    }

    Map<?,?> subMap = (Map<?,?>)o;
    return subMap.containsKey(subMapKey) && subMap.get(subMapKey).equals(subMapValue);
}

@Override
public void describeTo(Description description) {
    description.appendText(String.format("contains %s -> %s : %s", key, subMapKey, subMapValue));
}

public static SubMapMatcher containsSubMapWithKeyValue(String key, String subMapKey, String subMapValue) {
    return new SubMapMatcher(key, subMapKey, subMapValue);
}

}
Subroutine answered 20/7, 2015 at 18:46 Comment(2)
Thanks, this answer works. However, I think I would like to see if I can extend this to allow any level of nesting.Roundlet
Sounds easy enough, you can simple use a signature like Object value, String... keys. Then iterate through the keys, get the submap until you reach last one (or stumble upon a null or not-map as your value) and check the value. For example containsValueInSubMap( someValue, map0Key, map1Key, map2Key); Of course, personally I would NOT suggest using a standard Map for a key -> "value OR submap" mapping, but instead something more specific which would also allow have generic value types (a map with submaps and values is effectively a tree, should be easy to find something).Subroutine
L
6

If you only want to put Map<String, Object> as values in your outerMap adjust the declaration accordingly. Then you can do

@Test
public void testMapHasMap() {
    Map<String, Map<String, Object>> outerMap = new HashMap<>();
    Map<String, Object> nestedMap = new HashMap<String, Object>();
    nestedMap.put("foo", "bar");
    outerMap.put("nested", nestedMap);

    Object value = "bar";
    assertThat(outerMap, hasEntry(equalTo("nested"), hasEntry("foo", value)));  
}

Object value = "bar"; is necessary for compile reasons. Alternatively you could use

assertThat(outerMap,
   hasEntry(equalTo("nested"), Matchers.<String, Object> hasEntry("foo", "bar")));
Lindblad answered 21/7, 2015 at 5:40 Comment(1)
My example is a simplified version of my test in which the outerMap is a JSON object where not all the entries are maps, so I must keep the map declaration as written.Roundlet
D
5

If You declare outerMap as Map<String, Map<String, Object>> you don't need the ugly cast. Like this:

public class MapContainsMapTest {

    @Test
    public void testMapHasMap() {
        Map<String, Map<String, Object>> outerMap = new HashMap<>();
        Map<String, Object> nestedMap = new HashMap<>();
        nestedMap.put("foo", "bar");
        outerMap.put("nested", nestedMap);

        assertThat(outerMap.get("nested"), hasEntry("foo", "bar"));
    }
}
Decembrist answered 20/7, 2015 at 19:14 Comment(2)
@DanGetz if I remove the cast, it doesnt compile, outerMap must be declared as Map<String, Map<String, Object>>.Kippie
My example is a simplified version of my test in which the outerMap is a JSON object where not all the entries are maps, so I must keep the map declaration as written.Roundlet
S
4

I would probably extend a new Matcher for that, something like that (beware, NPEs lurking):

class SubMapMatcher extends BaseMatcher<Map<?,?>> {

private Object key;
private Object subMapKey;
private Object subMapValue;

public SubMapMatcher(Object key, Object subMapKey, Object subMapValue) {
    super();
    this.key = key;
    this.subMapKey = subMapKey;
    this.subMapValue = subMapValue;
}

@Override
public boolean matches(Object item) {

    Map<?,?> map = (Map<?,?>)item;

    if (!map.containsKey(key)) {
        return false;
    }

    Object o = map.get(key);

    if (!(o instanceof Map<?,?>)) {
        return false;
    }

    Map<?,?> subMap = (Map<?,?>)o;
    return subMap.containsKey(subMapKey) && subMap.get(subMapKey).equals(subMapValue);
}

@Override
public void describeTo(Description description) {
    description.appendText(String.format("contains %s -> %s : %s", key, subMapKey, subMapValue));
}

public static SubMapMatcher containsSubMapWithKeyValue(String key, String subMapKey, String subMapValue) {
    return new SubMapMatcher(key, subMapKey, subMapValue);
}

}
Subroutine answered 20/7, 2015 at 18:46 Comment(2)
Thanks, this answer works. However, I think I would like to see if I can extend this to allow any level of nesting.Roundlet
Sounds easy enough, you can simple use a signature like Object value, String... keys. Then iterate through the keys, get the submap until you reach last one (or stumble upon a null or not-map as your value) and check the value. For example containsValueInSubMap( someValue, map0Key, map1Key, map2Key); Of course, personally I would NOT suggest using a standard Map for a key -> "value OR submap" mapping, but instead something more specific which would also allow have generic value types (a map with submaps and values is effectively a tree, should be easy to find something).Subroutine
A
1

Try like this :

        assertThat(nestedMap).contains(Map.entry("foo", "bar"));
        assertThat(outerMap).contains(Map.entry("nested", nestedMap));
Atli answered 12/2, 2022 at 20:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.