Repository: assertj/assertj (Java)

Issue:
AssertJ cannot compare arrays in maps
**Describe the bug**
A clear and concise description of what the bug is.

* assertj core version: 3.25.0
* java version: 17
* test framework version: JUnit 5.10.1
* os (if relevant): MacOS Sonoma 14.2.1

**Test case reproducing the bug**

This code worked in AssertJ 3.24.2, but fails in AssertJ 3.25.0:

```java
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import java.util.Map;

  @Test
  void fails() {
    assertThat(Map.of("firstName", new String[] {"Bob"}))
      .containsExactlyEntriesOf(Map.of("firstName", new String[] {"Bob"}));
  }
```

This test fails with the error message:
```java
    java.lang.AssertionError: 
    Actual and expected have the same elements but not in the same order, at index 0 actual element was:
      "firstName"=["Bob"]
    whereas expected element was:
      "firstName"=["Bob"]
        at play.utils.UtilsTest.fails(UtilsTest.java:50)
```

Regression in `AbstractMapAssert.containsExactly` causes `NullPointerException`
**Describe the bug**

* assertj core version: 3.25.0

Commit 498ee5bb0a5cc367420cdb3c8e0e93d6e4bf5e51 introduced a regression in `AbstractMapAssert.containsExactly`.

Specifically, it is not always safe to put `null` into a `Map`.

https://github.com/assertj/assertj/blob/887f97b65da068e03308a6373a0c8a76912377aa/assertj-core/src/main/java/org/assertj/core/internal/Maps.java#L589

**Test case reproducing the bug**

The following now fails with a `NullPointerException`.

```java
@Test
void assertJBug() {
	Properties properties = new Properties();
	properties.setProperty("k1", "v1");
	properties.setProperty("k2", "v2");
	assertThat(properties).containsExactly(entry("k1", "v1"), entry("k2", "v2"));
}
```



Hints:
Thanks for reporting it, @asolntsev!
The change that causes the issue is 498ee5bb0a5cc367420cdb3c8e0e93d6e4bf5e51.
Here we assume that the map value can be compared with `equals`:

https://github.com/assertj/assertj/blob/25347d57bb28295548ec6bd5b06e70b6d03496c9/assertj-core/src/main/java/org/assertj/core/internal/Maps.java#L572

This cannot work with array values as `equals` compares identity and not the array's content.
**Team decision**: 498ee5bb0a5cc367420cdb3c8e0e93d6e4bf5e51 should be reverted and #2165 should be addressed again with the knowledge gathered from this issue.
Thanks for reporting it and the analysis, @sbrannen!
In case anyone else runs into this, you may find the workaround I came up with suitable for your use case as well.

https://github.com/spring-projects/spring-framework/commit/ffddbb586e60ce1b66a3b962612636db9c61ed81#diff-6f347f5b75cdbb58d65e5ec32c8e3ffe9abf719f50f3bc819ec257296d466aee

Basically, I created a new map (`LinkedHashMap` in this case) based on the original map (`Properties` in this case), and the `NullPointerException` is avoided since `LinkedHashMap` supports `null` values.
**Team decision**: 498ee5bb0a5cc367420cdb3c8e0e93d6e4bf5e51 should be reverted and #2165 should be addressed again with the knowledge gathered from this issue.

Patch:
diff --git a/assertj-core/src/main/java/org/assertj/core/internal/Maps.java b/assertj-core/src/main/java/org/assertj/core/internal/Maps.java
--- a/assertj-core/src/main/java/org/assertj/core/internal/Maps.java
+++ b/assertj-core/src/main/java/org/assertj/core/internal/Maps.java
@@ -440,10 +440,6 @@ private static <K, V> Map<K, V> clone(Map<K, V> map) throws NoSuchMethodExceptio
     }
   }
 
-  private static boolean isSingletonMap(Map<?, ?> map) {
-    return map.size() == 1;
-  }
-
   private static boolean isMultiValueMapAdapterInstance(Map<?, ?> map) {
     return isInstanceOf(map, "org.springframework.util.MultiValueMapAdapter");
   }
@@ -457,16 +453,6 @@ private static boolean isInstanceOf(Object object, String className) {
     }
   }
 
-  private static <K, V> Map<K, V> createEmptyMap(Map<K, V> map) {
-    try {
-      Map<K, V> cloned = clone(map);
-      cloned.clear();
-      return cloned;
-    } catch (NoSuchMethodException | RuntimeException e) {
-      return new LinkedHashMap<>();
-    }
-  }
-
   public <K, V> void assertContainsValue(AssertionInfo info, Map<K, V> actual, V value) {
     assertNotNull(info, actual);
     if (!containsValue(actual, value)) throw failures.failure(info, shouldContainValue(actual, value));
@@ -565,16 +551,6 @@ public <K, V> void assertContainsExactly(AssertionInfo info, Map<K, V> actual, E
     failIfEntriesIsEmptySinceActualIsNotEmpty(info, actual, entries);
     assertHasSameSizeAs(info, actual, entries);
 
-    if (isSingletonMap(actual)) {
-      // shortcut for any singleton map but specifically for org.apache.commons.collections4.map.SingletonMap that is immutable
-      // and fail when we try to remove elements from them in compareActualMapAndExpectedEntries
-      // we only have to compare the map unique element
-      if (!actual.containsKey(entries[0].getKey()) || !actual.containsValue(entries[0].getValue())) {
-        throw failures.failure(info, elementsDifferAtIndex(actual.entrySet().iterator().next(), entries[0], 0));
-      }
-      return;
-    }
-
     Set<Entry<? extends K, ? extends V>> notFound = new LinkedHashSet<>();
     Set<Entry<? extends K, ? extends V>> notExpected = new LinkedHashSet<>();
 
@@ -583,15 +559,11 @@ public <K, V> void assertContainsExactly(AssertionInfo info, Map<K, V> actual, E
     if (notExpected.isEmpty() && notFound.isEmpty()) {
       // check entries order
       int index = 0;
-      // Create a map with the same type as actual to use the Map built-in comparison, ex: maps string case insensitive keys.
-      Map<K, V> emptyMap = createEmptyMap(actual);
       for (K keyFromActual : actual.keySet()) {
-        emptyMap.put(keyFromActual, null);
-        if (!emptyMap.containsKey(entries[index].getKey())) {
+        if (!deepEquals(keyFromActual, entries[index].getKey())) {
           Entry<K, V> actualEntry = entry(keyFromActual, actual.get(keyFromActual));
           throw failures.failure(info, elementsDifferAtIndex(actualEntry, entries[index], index));
         }
-        emptyMap.remove(keyFromActual);
         index++;
       }
       // all entries are in the same order.
@@ -605,12 +577,7 @@ private <K, V> void compareActualMapAndExpectedEntries(Map<K, V> actual, Entry<?
                                                          Set<Entry<? extends K, ? extends V>> notExpected,
                                                          Set<Entry<? extends K, ? extends V>> notFound) {
     Map<K, V> expectedEntries = entriesToMap(entries);
-    Map<K, V> actualEntries = null;
-    try {
-      actualEntries = clone(actual);
-    } catch (NoSuchMethodException | RuntimeException e) {
-      actualEntries = new LinkedHashMap<>(actual);
-    }
+    Map<K, V> actualEntries = new LinkedHashMap<>(actual);
     for (Entry<K, V> entry : expectedEntries.entrySet()) {
       if (containsEntry(actualEntries, entry(entry.getKey(), entry.getValue()))) {
         // this is an expected entry


Test patch:
diff --git a/assertj-core/src/test/java/org/assertj/core/internal/MapsBaseTest.java b/assertj-core/src/test/java/org/assertj/core/internal/MapsBaseTest.java
--- a/assertj-core/src/test/java/org/assertj/core/internal/MapsBaseTest.java
+++ b/assertj-core/src/test/java/org/assertj/core/internal/MapsBaseTest.java
@@ -70,9 +70,6 @@ public class MapsBaseTest extends WithPlayerData {
                                                                                              MapsBaseTest::persistentMap,
                                                                                              MapsBaseTest::persistentSortedMap);
 
-  protected static final Supplier<Map<String, String>> PERSISTENT_MAP = MapsBaseTest::persistentMap;
-  protected static final Supplier<Map<String, String>> PERSISTENT_SORTED_MAP = MapsBaseTest::persistentSortedMap;
-
   private static <K, V> PersistentMap<K, V> persistentMap() {
     return hibernateMap(PersistentMap::new, HashMap::new);
   }
diff --git a/assertj-core/src/test/java/org/assertj/core/internal/maps/Maps_assertContainsExactly_Test.java b/assertj-core/src/test/java/org/assertj/core/internal/maps/Maps_assertContainsExactly_Test.java
--- a/assertj-core/src/test/java/org/assertj/core/internal/maps/Maps_assertContainsExactly_Test.java
+++ b/assertj-core/src/test/java/org/assertj/core/internal/maps/Maps_assertContainsExactly_Test.java
@@ -14,19 +14,16 @@
 
 import static java.util.Collections.emptyMap;
 import static java.util.Collections.singletonMap;
-import static java.util.Collections.unmodifiableMap;
-import static java.util.stream.Stream.concat;
-import static org.assertj.core.api.Assertions.assertThatNoException;
 import static org.assertj.core.api.Assertions.assertThatNullPointerException;
-import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
-import static org.assertj.core.api.BDDAssertions.entry;
 import static org.assertj.core.api.BDDAssertions.then;
+import static org.assertj.core.data.MapEntry.entry;
 import static org.assertj.core.error.ShouldBeEmpty.shouldBeEmpty;
 import static org.assertj.core.error.ShouldContainExactly.elementsDifferAtIndex;
 import static org.assertj.core.error.ShouldContainExactly.shouldContainExactly;
 import static org.assertj.core.error.ShouldHaveSameSizeAs.shouldHaveSameSizeAs;
 import static org.assertj.core.internal.ErrorMessages.entriesToLookForIsNull;
 import static org.assertj.core.test.Maps.mapOf;
+import static org.assertj.core.test.TestData.someInfo;
 import static org.assertj.core.util.Arrays.array;
 import static org.assertj.core.util.Arrays.asList;
 import static org.assertj.core.util.AssertionsUtil.assertThatAssertionErrorIsThrownBy;
@@ -34,35 +31,20 @@
 import static org.assertj.core.util.FailureMessages.actualIsNull;
 import static org.assertj.core.util.Lists.list;
 import static org.assertj.core.util.Sets.set;
-import static org.junit.jupiter.params.provider.Arguments.arguments;
 import static org.mockito.Mockito.verify;
 
-import java.util.HashMap;
-import java.util.IdentityHashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.function.Supplier;
-import java.util.stream.Stream;
+import java.util.Map.Entry;
+import java.util.Properties;
 
-import org.apache.commons.collections4.map.CaseInsensitiveMap;
-import org.apache.commons.collections4.map.SingletonMap;
+import org.assertj.core.api.AssertionInfo;
 import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
-import org.assertj.core.data.MapEntry;
 import org.assertj.core.internal.MapsBaseTest;
-import org.assertj.core.test.jdk11.Jdk11;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import com.google.common.collect.ImmutableMap;
 
 /**
- * Tests for
- * <code>{@link org.assertj.core.internal.Maps#assertContainsExactly(org.assertj.core.api.AssertionInfo, java.util.Map, java.util.Map.Entry...)}</code>
- * .
- *
  * @author Jean-Christophe Gay
  */
 class Maps_assertContainsExactly_Test extends MapsBaseTest {
@@ -80,9 +62,9 @@ void initLinkedHashMap() {
   void should_fail_if_actual_is_null() {
     // GIVEN
     actual = null;
-    MapEntry<String, String>[] expected = array(entry("name", "Yoda"));
+    Entry<String, String>[] expected = array(entry("name", "Yoda"));
     // WHEN
-    AssertionError assertionError = expectAssertionError(() -> maps.assertContainsExactly(info, actual, expected));
+    AssertionError assertionError = expectAssertionError(() -> maps.assertContainsExactly(someInfo(), actual, expected));
     // THEN
     then(assertionError).hasMessage(actualIsNull());
   }
@@ -90,16 +72,17 @@ void should_fail_if_actual_is_null() {
   @Test
   void should_fail_if_given_entries_array_is_null() {
     // GIVEN
-    MapEntry<String, String>[] entries = null;
+    Entry<String, String>[] entries = null;
     // WHEN/THEN
-    assertThatNullPointerException().isThrownBy(() -> maps.assertContainsExactly(info, linkedActual, entries))
+    assertThatNullPointerException().isThrownBy(() -> maps.assertContainsExactly(someInfo(), linkedActual, entries))
                                     .withMessage(entriesToLookForIsNull());
   }
 
   @Test
   void should_fail_if_given_entries_array_is_empty() {
     // GIVEN
-    MapEntry<String, String>[] expected = emptyEntries();
+    AssertionInfo info = someInfo();
+    Entry<String, String>[] expected = emptyEntries();
     // WHEN
     expectAssertionError(() -> maps.assertContainsExactly(info, actual, expected));
     // THEN
@@ -108,16 +91,18 @@ void should_fail_if_given_entries_array_is_empty() {
 
   @Test
   void should_pass_if_actual_and_entries_are_empty() {
-    maps.assertContainsExactly(info, emptyMap(), array());
+    maps.assertContainsExactly(someInfo(), emptyMap(), array());
   }
 
   @Test
   void should_pass_if_actual_contains_given_entries_in_order() {
-    maps.assertContainsExactly(info, linkedActual, array(entry("name", "Yoda"), entry("color", "green")));
+    maps.assertContainsExactly(someInfo(), linkedActual, array(entry("name", "Yoda"), entry("color", "green")));
   }
 
   @Test
   void should_fail_if_actual_contains_given_entries_in_disorder() {
+    // GIVEN
+    AssertionInfo info = someInfo();
     // WHEN
     expectAssertionError(() -> maps.assertContainsExactly(info, linkedActual,
                                                           array(entry("color", "green"), entry("name", "Yoda"))));
@@ -128,7 +113,8 @@ void should_fail_if_actual_contains_given_entries_in_disorder() {
   @Test
   void should_fail_if_actual_and_expected_entries_have_different_size() {
     // GIVEN
-    MapEntry<String, String>[] expected = array(entry("name", "Yoda"));
+    AssertionInfo info = someInfo();
+    Entry<String, String>[] expected = array(entry("name", "Yoda"));
     // WHEN
     ThrowingCallable code = () -> maps.assertContainsExactly(info, linkedActual, expected);
     // THEN
@@ -139,8 +125,9 @@ void should_fail_if_actual_and_expected_entries_have_different_size() {
   @Test
   void should_fail_if_actual_does_not_contains_every_expected_entries_and_contains_unexpected_one() {
     // GIVEN
-    MapEntry<String, String>[] expected = array(entry("name", "Yoda"), entry("color", "green"));
-    Map<String, String> underTest = newLinkedHashMap(entry("name", "Yoda"), entry("job", "Jedi"));
+    AssertionInfo info = someInfo();
+    Entry<String, String>[] expected = array(entry("name", "Yoda"), entry("color", "green"));
+    Map<String, String> underTest = mapOf(entry("name", "Yoda"), entry("job", "Jedi"));
     // WHEN
     expectAssertionError(() -> maps.assertContainsExactly(info, underTest, expected));
     // THEN
@@ -152,7 +139,8 @@ void should_fail_if_actual_does_not_contains_every_expected_entries_and_contains
   @Test
   void should_fail_if_actual_contains_entry_key_with_different_value() {
     // GIVEN
-    MapEntry<String, String>[] expectedEntries = array(entry("name", "Yoda"), entry("color", "yellow"));
+    AssertionInfo info = someInfo();
+    Entry<String, String>[] expectedEntries = array(entry("name", "Yoda"), entry("color", "yellow"));
     // WHEN
     expectAssertionError(() -> maps.assertContainsExactly(info, actual, expectedEntries));
     // THEN
@@ -161,146 +149,48 @@ void should_fail_if_actual_contains_entry_key_with_different_value() {
                                                         set(entry("color", "green"))));
   }
 
-  @ParameterizedTest
-  @MethodSource({
-      "orderedSensitiveSuccessfulArguments",
-      "orderedInsensitiveSuccessfulArguments",
-      "unorderedSensitiveSuccessfulArguments",
-      "unorderedInsensitiveSuccessfulArguments"
-  })
-  void should_pass(Map<String, String> map, MapEntry<String, String>[] entries) {
-    assertThatNoException().as(map.getClass().getName())
-                           .isThrownBy(() -> maps.assertContainsExactly(info, map, entries));
-  }
-
-  @ParameterizedTest
-  @MethodSource({
-      "orderedSensitiveFailureArguments",
-      "orderedInsensitiveFailureArguments",
-      "unorderedSensitiveFailureArguments",
-      "unorderedInsensitiveFailureArguments"
-  })
-  void should_fail(Map<String, String> map, MapEntry<String, String>[] entries) {
-    assertThatExceptionOfType(AssertionError.class).as(map.getClass().getName())
-                                                   .isThrownBy(() -> maps.assertContainsExactly(info, map, entries));
-  }
-
-  private static Stream<MapEntry<String, String>[]> orderedFailureTestCases() {
-    return Stream.of(array(entry("potato", "vegetable")),
-                     array(entry("banana", "fruit"), entry("potato", "vegetable"), entry("tomato", "vegetable")),
-                     array(entry("banana", "fruit"), entry("tomato", "vegetable")),
-                     array(entry("banana", "fruit"), entry("potato", "food")),
-                     array(entry("potato", "vegetable"), entry("banana", "fruit")));
-  }
-
-  private static Stream<MapEntry<String, String>[]> orderedSuccessTestCases() {
-    return Stream.<MapEntry<String, String>[]> of(array(entry("banana", "fruit"), entry("poTATo", "vegetable")));
-  }
-
-  private static Stream<MapEntry<String, String>[]> unorderedFailureTestCases() {
-    return Stream.of(array(entry("banana", "fruit"), entry("potato", "vegetable")),
-                     array(entry("strawberry", "fruit")),
-                     array(entry("banana", "food")),
-                     array());
-  }
-
-  private static Stream<MapEntry<String, String>[]> unorderedSuccessTestCases() {
-    return Stream.<MapEntry<String, String>[]> of(array(entry("banana", "fruit")));
-  }
-
-  private static Stream<MapEntry<String, String>[]> unorderedInsensitiveFailureTestCases() {
-    return Stream.<MapEntry<String, String>[]> of(array(entry("banana", "FRUIT")));
-  }
-
-  private static Stream<MapEntry<String, String>[]> unorderedInsensitiveSuccessTestCases() {
-    return Stream.<MapEntry<String, String>[]> of(array(entry("BANANA", "fruit")));
-  }
-
-  private static Stream<MapEntry<String, String>[]> orderedInsensitiveFailureTestCases() {
-    return Stream.of(array(entry("banana", "fruit"), entry("tomato", "vegetable")),
-                     array(entry("potato", "vegetable"), entry("BANANA", "fruit")),
-                     array(entry("banana", "vegetable"), entry("tomato", "fruit")),
-                     array(entry("banana", "plane"), entry("poTATo", "train")));
-  }
-
-  private static Stream<MapEntry<String, String>[]> orderedInsensitiveSuccessTestCases() {
-    return Stream.<MapEntry<String, String>[]> of(array(entry("BANANA", "fruit"), entry("potato", "vegetable")));
-  }
-
-  private static Stream<Arguments> orderedSensitiveSuccessfulArguments() {
-    Stream<Map<String, String>> maps = Stream.of(LinkedHashMap::new, PERSISTENT_SORTED_MAP)
-                                             .map(supplier -> mapOf(supplier,
-                                                                    entry("banana", "fruit"),
-                                                                    entry("poTATo", "vegetable")));
-    return mapsAndEntriesToArguments(maps, Maps_assertContainsExactly_Test::orderedSuccessTestCases);
-  }
-
-  private static Stream<Arguments> orderedInsensitiveSuccessfulArguments() {
-    Stream<Map<String, String>> maps = Stream.of(CASE_INSENSITIVE_MAPS)
-                                             .map(supplier -> mapOf(supplier,
-                                                                    entry("banana", "fruit"),
-                                                                    entry("poTATo", "vegetable")));
-    return mapsAndEntriesToArguments(maps, () -> concat(orderedSuccessTestCases(), orderedInsensitiveSuccessTestCases()));
-  }
-
-  private static Stream<Arguments> orderedSensitiveFailureArguments() {
-    Stream<Map<String, String>> maps = Stream.of(LinkedHashMap::new, PERSISTENT_SORTED_MAP)
-                                             .map(supplier -> mapOf(supplier,
-                                                                    entry("banana", "fruit"),
-                                                                    entry("poTATo", "vegetable")));
-    return mapsAndEntriesToArguments(maps, Maps_assertContainsExactly_Test::orderedFailureTestCases);
-  }
-
-  private static Stream<Arguments> orderedInsensitiveFailureArguments() {
-    Stream<Map<String, String>> maps = Stream.of(CASE_INSENSITIVE_MAPS)
-                                             .map(supplier -> mapOf(supplier, entry("banana", "fruit"),
-                                                                    entry("poTATo", "vegetable")));
-    return mapsAndEntriesToArguments(maps, () -> concat(orderedFailureTestCases(), orderedInsensitiveFailureTestCases()));
-  }
-
-  private static Stream<Arguments> unorderedSensitiveSuccessfulArguments() {
-    Stream<Map<String, String>> maps = concat(Stream.of(HashMap::new, IdentityHashMap::new, PERSISTENT_MAP)
-                                                    .map(supplier -> mapOf(supplier, entry("banana", "fruit"))),
-                                              Stream.of(singletonMap("banana", "fruit"),
-                                                        new SingletonMap<>("banana", "fruit"),
-                                                        unmodifiableMap(mapOf(entry("banana", "fruit"))),
-                                                        ImmutableMap.of("banana", "fruit"),
-                                                        Jdk11.Map.of("banana", "fruit")));
-    return mapsAndEntriesToArguments(maps, Maps_assertContainsExactly_Test::unorderedSuccessTestCases);
-  }
-
-  private static Stream<Arguments> unorderedInsensitiveSuccessfulArguments() {
-    Stream<Map<String, String>> maps = Stream.of(mapOf(CaseInsensitiveMap::new, entry("banana", "fruit")));
-    return mapsAndEntriesToArguments(maps, () -> concat(unorderedSuccessTestCases(), unorderedInsensitiveSuccessTestCases()));
+  @Test
+  void should_pass_with_singleton_map_having_array_value() {
+    // GIVEN
+    Map<String, String[]> actual = singletonMap("color", array("yellow"));
+    Entry<String, String[]>[] expected = array(entry("color", array("yellow")));
+    // WHEN/THEN
+    maps.assertContainsExactly(someInfo(), actual, expected);
   }
 
-  private static Stream<Arguments> unorderedSensitiveFailureArguments() {
-    Stream<Map<String, String>> maps = concat(Stream.of(HashMap::new, IdentityHashMap::new, PERSISTENT_MAP)
-                                                    .map(supplier -> mapOf(supplier, entry("banana", "fruit"))),
-                                              Stream.of(singletonMap("banana", "fruit"),
-                                                        new SingletonMap<>("banana", "fruit"),
-                                                        unmodifiableMap(mapOf(entry("banana", "fruit"))),
-                                                        ImmutableMap.of("banana", "fruit"),
-                                                        Jdk11.Map.of("banana", "fruit")));
-    return mapsAndEntriesToArguments(maps, Maps_assertContainsExactly_Test::unorderedInsensitiveSuccessTestCases);
+  @Test
+  void should_fail_with_singleton_map_having_array_value() {
+    // GIVEN
+    Map<String, String[]> actual = singletonMap("color", array("yellow"));
+    Entry<String, String[]>[] expected = array(entry("color", array("green")));
+    // WHEN
+    AssertionError assertionError = expectAssertionError(() -> maps.assertContainsExactly(someInfo(), actual, expected));
+    // THEN
+    then(assertionError).hasMessage(shouldContainExactly(actual, asList(expected),
+                                                         set(entry("color", array("green"))),
+                                                         set(entry("color", array("yellow")))).create());
   }
 
-  private static Stream<Arguments> unorderedInsensitiveFailureArguments() {
-    Stream<Map<String, String>> maps = Stream.of(mapOf(CaseInsensitiveMap::new, entry("banana", "fruit")));
-    return mapsAndEntriesToArguments(maps, () -> concat(unorderedFailureTestCases(), unorderedInsensitiveFailureTestCases()));
+  @Test
+  void should_pass_with_Properties() {
+    // GIVEN
+    Properties actual = mapOf(Properties::new, entry("name", "Yoda"), entry("job", "Jedi"));
+    Entry<Object, Object>[] expected = array(entry("name", "Yoda"), entry("job", "Jedi"));
+    // WHEN/THEN
+    maps.assertContainsExactly(info, actual, expected);
   }
 
-  private static Stream<Arguments> mapsAndEntriesToArguments(Stream<Map<String, String>> maps,
-                                                             Supplier<Stream<MapEntry<String, String>[]>> entries) {
-    return maps.flatMap(m -> entries.get().map(entryArray -> arguments(m, entryArray)));
+  @Test
+  void should_fail_with_Properties() {
+    // GIVEN
+    Properties actual = mapOf(Properties::new, entry("name", "Yoda"), entry("job", "Jedi"));
+    Entry<Object, Object>[] expected = array(entry("name", "Yoda"), entry("color", "green"));
+    // WHEN
+    AssertionError assertionError = expectAssertionError(() -> maps.assertContainsExactly(info, actual, expected));
+    // THEN
+    then(assertionError).hasMessage(shouldContainExactly(actual, asList(expected),
+                                                         set(entry("color", "green")),
+                                                         set(entry("job", "Jedi"))).create());
   }
 
-  @SafeVarargs
-  private static Map<String, String> newLinkedHashMap(MapEntry<String, String>... entries) {
-    LinkedHashMap<String, String> result = new LinkedHashMap<>();
-    for (MapEntry<String, String> entry : entries) {
-      result.put(entry.key, entry.value);
-    }
-    return result;
-  }
 }
diff --git a/assertj-core/src/test/java/org/assertj/core/internal/maps/Maps_assertContainsKeys_Test.java b/assertj-core/src/test/java/org/assertj/core/internal/maps/Maps_assertContainsKeys_Test.java
--- a/assertj-core/src/test/java/org/assertj/core/internal/maps/Maps_assertContainsKeys_Test.java
+++ b/assertj-core/src/test/java/org/assertj/core/internal/maps/Maps_assertContainsKeys_Test.java
@@ -32,6 +32,7 @@
 import static org.junit.jupiter.params.provider.Arguments.arguments;
 
 import java.util.Map;
+import java.util.Properties;
 import java.util.Set;
 import java.util.stream.Stream;
 
@@ -82,6 +83,27 @@ void should_fail_if_given_keys_array_is_empty() {
     then(thrown).isInstanceOf(IllegalArgumentException.class).hasMessage(keysToLookForIsEmpty("array of keys"));
   }
 
+  @Test
+  void should_pass_with_Properties() {
+    // GIVEN
+    Properties actual = mapOf(Properties::new, entry("name", "Yoda"), entry("job", "Jedi"));
+    Object[] expected = array("name", "job");
+    // WHEN/THEN
+    maps.assertContainsKeys(info, actual, expected);
+  }
+
+  @Test
+  void should_fail_with_Properties() {
+    // GIVEN
+    Properties actual = mapOf(Properties::new, entry("name", "Yoda"), entry("job", "Jedi"));
+    Object[] expected = array("name", "color");
+    Set<Object> notFound = set("color");
+    // WHEN
+    AssertionError assertionError = expectAssertionError(() -> maps.assertContainsKeys(info, actual, expected));
+    // THEN
+    then(assertionError).hasMessage(shouldContainKeys(actual, notFound).create());
+  }
+
   @ParameterizedTest
   @MethodSource({
       "unmodifiableMapsSuccessfulTestCases",