Repository: assertj/assertj (Java)

Issue:
Assertions ignore missing entry on maps
#### Summary
A custom assertion extending `MapAssert` that performs a `org.assertj.core.api.AbstractObjectAssert.hasFieldOrProperty(String)` on an `actual` that neither has a getter or field, but _is_ a `Map` silently ignores missing map entries.

#### Example
Given JSON:
```json
{
  "startLevel" : 1,
  "initialBundleStartLevel" : 1
}
```
The following will silently succeed
```java
net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson(
    json
).isObject().hasFieldOrProperty(
    "lastModified"
);
```
#### Reason
The method `org.assertj.core.util.introspection.PropertyOrFieldSupport.getSimpleValue(String, Object)` throws in the case of propertySupport not having a getter method, and fieldSupport not finding a field. However in the case of the Map fallback it does not fail for a missing entry.
```
    // try to get name as a property, then try as a field, then try as a map key
    try {
      return propertySupport.propertyValueOf(name, Object.class, input);
    } catch (IntrospectionError propertyIntrospectionError) {
      // no luck as a property, let's try as a field
      try {
        return fieldSupport.fieldValue(name, Object.class, input);
      } catch (IntrospectionError fieldIntrospectionError) {
        // neither field nor property found with given name

        // if the input object is a map, try name as a map key
        if (input instanceof Map) {
          Map<?, ?> map = (Map<?, ?>) input;
          return map.get(name);
        }

        // no value found with given name, it is considered as an error
        String message = format("%nCan't find any field or property with name '%s'.%n" +
                                "Error when introspecting properties was :%n" +
                                "- %s %n" +
                                "Error when introspecting fields was :%n" +
                                "- %s",
                                name, propertyIntrospectionError.getMessage(),
                                fieldIntrospectionError.getMessage());
        throw new IntrospectionError(message, fieldIntrospectionError);
      }
    }
```


Hints:
Thanks for the detailed report @rotty3000.

is there a particular reason why you are using `hasFieldOrProperty` instead of `containsKey`? The `contains*` API is also demonstrated in JsonUnit's [examples](https://github.com/lukas-krecan/JsonUnit#assertj-integration).

Still, it is interesting that `AbstractMapAssert` is inheriting from `AbstractObjectAssert`, exposing assertions from the latter. I would imagine those assertions don't make much sense with maps.

Playing around:

```java
assertThat(new Object()).hasFieldOrProperty("lastModified"); // fails

assertThat(new HashMap<>()).hasFieldOrProperty("lastModified"); // succeeds

assertThat(new HashMap<>()).containsKey("lastModified"); // fails
```

@assertj/core should we consider a review of the class hierarchy in this area? My current feeling would be that `AbstractMapAssert` should inherit from `AbstractAssert` only.
Thank you for the insight @scordio. I really appreciate it.

However, from a outsiders perspective looking at Json "objects" it's not clear that the underlying structure is a `Map` (even though that is the obvious implementation) and so the nomenclature that a JSON "object" has a _property_ of _field_ is far more natural than it having a "key". And so in this sense I feel that, if the API is there, it should work as expected. Don't you agree? I don't think the developer of JsonUnit's assertj support would disagree :)
I'd like to propose an implementation of `MapSupport` akin to `FieldSupport` and `PropertySupport` having a property `allowMissingKey` where the default is true and this is the default in the EXTRACTING use case. Meanwhile in the comparison use case it could be set to false.
> Thank you for the insight @scordio. I really appreciate it.
> 
> However, from a outsiders perspective looking at Json "objects" it's not clear that the underlying structure is a `Map` (even though that is the obvious implementation) and so the nomenclature that a JSON "object" has a _property_ of _field_ is far more natural than it having a "key". And so in this sense I feel that, if the API is there, it should work as expected. Don't you agree? I don't think the developer of JsonUnit's assertj support would disagree :)

I agree that the external layer focuses on the JSON, hence`hasFieldOrProperty` might sound better here. However, `assertj-core` has no knowledge about the JSON type, that is just a map. The JSON type is the target of JsonUnit.

Having said that, if we would decide to change the API exposed by `AbstractMapAssert`, I would definitely brainstorm together with @lukas-krecan in order to keep a meaningful API for JsonUnit users.

Let's see what the rest of the team thinks about it.
So, I wanted to add that after further consideration I think I'm probably OK with using the `contains(entry(name, value))`, `containsKey(name)` nomenclature. They seems to handle the cases I need well enough. However, I do still find it strange that `hasFieldOrPropertyWithValue(name, value)` seems to work very well while `hasFieldOrProperty(name)` does not work reliably.

I suppose it just caught me off guard when I realized after having written several positive tests and then a negative test only to realize the negative case wasn't failing. That forced me to go back and reassess every test I had written to make sure I didn't accidentally have a false positive case hiding in the weeds.
`hasFieldOrPropertyWithValue(name, value)` and `hasFieldOrProperty(name)` should be consistent and fail if there is no field or property or key with the given name.

> My current feeling would be that `AbstractMapAssert` should inherit from `AbstractAssert` only.

That would be a breaking change for sure, I'll try to find why we have it, I don't remember right now.
@rotty3000 and the AssertJ team. Doesn't it make more sense to request the json-unit team to look into this. Calling that `isObject` actually returns a `JsonMapAssert` from json-unit. I think it would make more sense address and improve the API from json-unit here.
Fair point @filiphr, I did not realize it was returning a `JsonMapAssert`.
But still, we should be consistent between `hasFieldOrPropertyWithValue` and `hasFieldOrProperty`
There are two issues. One is that `hasFieldOrPropertyWithValue(name, value)` and `hasFieldOrProperty(name)` are inconsistent and their behavior is surprising for MapAssert.

The other is that JsonUnit may lead user to use them. I will not be able to help with the first, but I can take a look at JsonUnit issues. I may mark the methods as deprecated or fix their behavior.


> Doesn't it make more sense to request the json-unit team to look into this.

That was my initial feeling, but we might also have a conceptual issue with the current `AbstractMapAssert` parent class.

> That would be a breaking change for sure, I'll try to find why we have it, I don't remember right now.

Yes, definitely. If we realize this is the way to go, I would deprecate `AbstractObjectAssert`'s methods in `AbstractMapAssert`, similarly to what @lukas-krecan proposed for JsonUnit, and we could change the inheritance in version 4.

> we should be consistent between `hasFieldOrPropertyWithValue` and `hasFieldOrProperty`

I'll look into it.
> I do still find it strange that `hasFieldOrPropertyWithValue(name, value)` seems to work very well while `hasFieldOrProperty(name)` does not work reliably.

@rotty3000 do you have an example where `hasFieldOrPropertyWithValue(name, value)` is working correctly with a `Map`?

I played a bit more and both assertions below are erroneous but "consistent" 🙂 

```java
assertThat(new HashMap<>()).hasFieldOrProperty("lastModified"); // succeeds
assertThat(new HashMap<>()).hasFieldOrPropertyWithValue("lastModified", null); // succeeds
```

Also, trying:

```java
assertThat(new HashMap<>()).hasFieldOrPropertyWithValue("lastModified", "value"); // fails
```

yields:

```
java.lang.AssertionError: 
Expecting
  {}
to have a property or a field named "lastModified" with value
  "value"
but value was:
  null
(static and synthetic fields are ignored)
```

From my point of view, both `hasFieldOrProperty` and `hasFieldOrPropertyWithValue` when actual is a `Map` should be fixed.

Patch:
diff --git a/src/main/java/org/assertj/core/util/introspection/PropertyOrFieldSupport.java b/src/main/java/org/assertj/core/util/introspection/PropertyOrFieldSupport.java
--- a/src/main/java/org/assertj/core/util/introspection/PropertyOrFieldSupport.java
+++ b/src/main/java/org/assertj/core/util/introspection/PropertyOrFieldSupport.java
@@ -74,7 +74,7 @@ public Object getSimpleValue(String name, Object input) {
         // if the input object is a map, try name as a map key
         if (input instanceof Map) {
           Map<?, ?> map = (Map<?, ?>) input;
-          return map.get(name);
+          if (map.containsKey(name)) return map.get(name);
         }
 
         // no value found with given name, it is considered as an error


Test patch:
diff --git a/src/test/java/org/assertj/core/api/map/MapAssert_extracting_Test.java b/src/test/java/org/assertj/core/api/map/MapAssert_extracting_Test.java
--- a/src/test/java/org/assertj/core/api/map/MapAssert_extracting_Test.java
+++ b/src/test/java/org/assertj/core/api/map/MapAssert_extracting_Test.java
@@ -13,13 +13,15 @@
 package org.assertj.core.api.map;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.assertj.core.api.BDDAssertions.then;
+import static org.assertj.core.util.AssertionsUtil.expectAssertionError;
 import static org.assertj.core.util.FailureMessages.actualIsNull;
 
 import java.util.HashMap;
 import java.util.Map;
 
+import org.assertj.core.util.introspection.IntrospectionError;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -34,68 +36,91 @@ void setup() {
     map = new HashMap<>();
     map.put(NAME, "kawhi");
     map.put("age", 25);
+    map.put("title", null);
   }
 
   @Test
   void should_allow_assertions_on_values_extracted_from_given_map_keys() {
+    // WHEN/THEN
     assertThat(map).extracting(NAME, "age")
-                   .contains("kawhi", 25);
+                   .containsExactly("kawhi", 25);
   }
 
   @Test
   void should_allow_object_assertions_on_value_extracted_from_given_map_key() {
+    // WHEN/THEN
     assertThat(map).extracting(NAME)
                    .isEqualTo("kawhi");
   }
 
   @Test
   void should_allow_assertions_on_values_extracted_from_given_extractors() {
+    // WHEN/THEN
     assertThat(map).extracting(m -> m.get(NAME), m -> m.get("age"))
-                   .contains("kawhi", 25);
+                   .containsExactly("kawhi", 25);
   }
 
   @Test
   void should_allow_object_assertions_on_value_extracted_from_given_extractor() {
+    // WHEN/THEN
     assertThat(map).extracting(m -> m.get(NAME))
                    .isEqualTo("kawhi");
   }
 
   @Test
   void should_extract_null_element_from_unknown_key() {
+    // WHEN/THEN
     assertThat(map).extracting(NAME, "unknown")
-                   .contains("kawhi", (Object) null);
+                   .containsExactly("kawhi", null);
   }
 
   @Test
-  void should_extract_null_object_from_unknown_key() {
-    assertThat(map).extracting("unknown")
+  void should_extract_null_object_from_key_with_null_value() {
+    // WHEN/THEN
+    assertThat(map).extracting("title")
                    .isNull();
   }
 
+  @Test
+  void should_fail_with_unknown_key() {
+    // WHEN
+    Throwable thrown = catchThrowable(() -> assertThat(map).extracting("unknown"));
+    // THEN
+    then(thrown).isInstanceOf(IntrospectionError.class);
+  }
+
   @Test
   void should_use_key_names_as_description() {
-    assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(map).extracting(NAME, "age").isEmpty())
-                                                   .withMessageContaining("[Extracted: name, age]");
+    // WHEN
+    AssertionError error = expectAssertionError(() -> assertThat(map).extracting(NAME, "age").isEmpty());
+    // THEN
+    then(error).hasMessageContaining("[Extracted: name, age]");
   }
 
   @Test
   void should_use_key_name_as_description() {
-    assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(map).extracting(NAME).isNull())
-                                                   .withMessageContaining("[Extracted: name]");
+    // WHEN
+    AssertionError error = expectAssertionError(() -> assertThat(map).extracting(NAME).isNull());
+    // THEN
+    then(error).hasMessageContaining("[Extracted: name]");
   }
 
   @Test
   void should_keep_existing_description_if_set_when_extracting_values_list() {
-    assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(map).as("check name and age")
-                                                                                    .extracting(NAME, "age").isEmpty())
-                                                   .withMessageContaining("[check name and age]");
+    // WHEN
+    AssertionError error = expectAssertionError(() -> assertThat(map).as("check name and age")
+                                                                     .extracting(NAME, "age").isEmpty());
+    // THEN
+    then(error).hasMessageContaining("[check name and age]");
   }
 
   @Test
   void should_keep_existing_description_if_set_when_extracting_value_object() {
-    assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(map).as("check name")
-                                                                                    .extracting(NAME).isNull())
-                                                   .withMessageContaining("[check name]");
+    // WHEN
+    AssertionError error = expectAssertionError(() -> assertThat(map).as("check name")
+                                                                     .extracting(NAME).isNull());
+    // THEN
+    then(error).hasMessageContaining("[check name]");
   }
 
   @Test
@@ -103,9 +128,9 @@ void should_fail_with_key_list_if_actual_is_null() {
     // GIVEN
     map = null;
     // WHEN
-    Throwable error = catchThrowable(() -> assertThat(map).extracting(NAME, "age"));
+    AssertionError error = expectAssertionError(() -> assertThat(map).extracting(NAME, "age"));
     // THEN
-    assertThat(error).hasMessage(actualIsNull());
+    then(error).hasMessage(actualIsNull());
   }
 
   @Test
@@ -113,9 +138,9 @@ void should_fail_with_single_key_if_actual_is_null() {
     // GIVEN
     map = null;
     // WHEN
-    Throwable error = catchThrowable(() -> assertThat(map).extracting(NAME));
+    AssertionError error = expectAssertionError(() -> assertThat(map).extracting(NAME));
     // THEN
-    assertThat(error).hasMessage(actualIsNull());
+    then(error).hasMessage(actualIsNull());
   }
 
 }
diff --git a/src/test/java/org/assertj/core/api/map/MapAssert_flatExtracting_Test.java b/src/test/java/org/assertj/core/api/map/MapAssert_flatExtracting_Test.java
--- a/src/test/java/org/assertj/core/api/map/MapAssert_flatExtracting_Test.java
+++ b/src/test/java/org/assertj/core/api/map/MapAssert_flatExtracting_Test.java
@@ -14,7 +14,10 @@
 
 import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.assertj.core.api.BDDAssertions.then;
 import static org.assertj.core.util.Arrays.array;
+import static org.assertj.core.util.Lists.list;
 import static org.assertj.core.util.Sets.newLinkedHashSet;
 
 import java.util.LinkedHashMap;
@@ -22,6 +25,7 @@
 import java.util.Map;
 
 import org.assertj.core.api.MapAssert;
+import org.assertj.core.util.introspection.IntrospectionError;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -45,6 +49,7 @@ void beforeEachTest() {
     map.put("job", jobs);
     map.put("city", cities);
     map.put("rank", ranks);
+    map.put("id", list(null, null));
   }
 
   @Test
@@ -57,16 +62,25 @@ void should_allow_assertions_on_flattened_values_extracted_from_given_map_keys()
   }
 
   @Test
-  void should_extract_null_from_unknown_key() {
-    assertThat(map).flatExtracting("name", "id", "city")
-                   .containsExactly("Dave", "Jeff", null, "Dover", "Boston", "Paris");
-    assertThat(map).flatExtracting("foo", "bar")
-                   .containsOnlyNulls();
+  void should_extract_null_from_key_with_flattened_null_values() {
+    // WHEN/THEN
+    assertThat(map).flatExtracting("name", "city", "id")
+                   .containsExactly("Dave", "Jeff", "Dover", "Boston", "Paris", null, null);
+  }
+
+  @Test
+  void should_fail_if_key_is_unknown() {
+    // WHEN
+    Throwable thrown = catchThrowable(() -> assertThat(map).flatExtracting("foo", "bar"));
+    // THEN
+    then(thrown).isInstanceOf(IntrospectionError.class);
   }
 
   @Test
   void should_extract_but_not_flatten_non_collection_values() {
+    // GIVEN
     map.put("year", 2017);
+    // WHEN/THEN
     assertThat(map).flatExtracting("name", "job", "year")
                    .containsExactly("Dave", "Jeff", "Plumber", "Builder", 2017);
   }
diff --git a/src/test/java/org/assertj/core/api/object/ObjectAssert_extracting_with_String_Array_Test.java b/src/test/java/org/assertj/core/api/object/ObjectAssert_extracting_with_String_Array_Test.java
new file mode 100644
--- /dev/null
+++ b/src/test/java/org/assertj/core/api/object/ObjectAssert_extracting_with_String_Array_Test.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ *
+ * Copyright 2012-2021 the original author or authors.
+ */
+package org.assertj.core.api.object;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.assertj.core.api.BDDAssertions.then;
+import static org.assertj.core.util.AssertionsUtil.expectAssertionError;
+import static org.assertj.core.util.BigDecimalComparator.BIG_DECIMAL_COMPARATOR;
+
+import java.math.BigDecimal;
+
+import org.assertj.core.api.ObjectAssert;
+import org.assertj.core.test.Employee;
+import org.assertj.core.test.Name;
+import org.assertj.core.util.introspection.IntrospectionError;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for <code>{@link ObjectAssert#extracting(String[])}</code>.
+ */
+@DisplayName("ObjectAssert extracting(String[])")
+class ObjectAssert_extracting_with_String_Array_Test {
+
+  private Employee luke;
+  private Employee leia;
+
+  @BeforeEach
+  void setup() {
+    luke = new Employee(2L, new Name("Luke", "Skywalker"), 26);
+    luke.setAttribute("side", "light");
+    leia = new Employee(2L, new Name("Leia", "Skywalker"), 26);
+    luke.setRelation("brother", null);
+    luke.setRelation("sister", leia);
+    leia.setRelation("brother", luke);
+  }
+
+  @Test
+  void should_allow_assertions_on_array_of_properties_extracted_from_given_object_by_name() {
+    // WHEN/THEN
+    assertThat(luke).extracting("id", "name")
+                    .hasSize(2)
+                    .doesNotContainNull();
+  }
+
+  @Test
+  void should_allow_assertions_on_array_of_nested_properties_extracted_from_given_object_by_name() {
+    // WHEN/THEN
+    assertThat(luke).extracting("name.first", "name.last")
+                    .hasSize(2)
+                    .containsExactly("Luke", "Skywalker");
+  }
+
+  @Test
+  void should_allow_assertion_on_mixed_properties_or_fields_with_nested_map_values() {
+    // WHEN/THEN
+    assertThat(luke).extracting("id", "name.last", "attributes.side", "relations.sister", "relations.sister.relations.brother.id")
+                    .containsExactly(2L, "Skywalker", "light", leia, 2L);
+  }
+
+  @Test
+  void should_follow_map_get_behavior_for_key_with_null_value() {
+    // WHEN/THEN
+    assertThat(luke).extracting("relations.brother", "relations.sister", "relations.sister.name.first")
+                    .containsExactly(null, leia, "Leia");
+  }
+
+  @Test
+  void should_throw_IntrospectionError_if_nested_map_key_does_not_exist() {
+    // WHEN
+    Throwable thrown = catchThrowable(() -> assertThat(luke).extracting("relations.unknown", "relations.sister"));
+    // THEN
+    then(thrown).isInstanceOf(IntrospectionError.class);
+  }
+
+  @Test
+  void should_use_property_field_names_as_description_when_extracting_tuples_list() {
+    // WHEN
+    AssertionError error = expectAssertionError(() -> assertThat(luke).extracting("name.first", "name.last")
+                                                                      .isEmpty());
+    // THEN
+    then(error).hasMessageContaining("[Extracted: name.first, name.last]");
+  }
+
+  @Test
+  void should_keep_existing_description_if_set_when_extracting_tuples_list() {
+    // WHEN
+    AssertionError error = expectAssertionError(() -> assertThat(luke).as("check luke first and last name")
+                                                                      .extracting("name.first", "name.last")
+                                                                      .isEmpty());
+    // THEN
+    then(error).hasMessageContaining("[check luke first and last name]");
+  }
+
+  @Test
+  void should_allow_to_specify_type_comparator_after_using_extracting_with_multiple_parameters_on_object() {
+    // GIVEN
+    Person obiwan = new Person("Obi-Wan");
+    obiwan.setHeight(new BigDecimal("1.820"));
+    // WHEN/THEN
+    assertThat(obiwan).extracting("name", "height")
+                      .usingComparatorForType(BIG_DECIMAL_COMPARATOR, BigDecimal.class)
+                      .containsExactly("Obi-Wan", new BigDecimal("1.82"));
+  }
+
+  @SuppressWarnings("unused")
+  private static class Person {
+
+    private final String name;
+    private BigDecimal height;
+
+    public Person(String name) {
+      this.name = name;
+    }
+
+    public void setHeight(BigDecimal height) {
+      this.height = height;
+    }
+  }
+
+}
diff --git a/src/test/java/org/assertj/core/api/object/ObjectAssert_extracting_Test.java b/src/test/java/org/assertj/core/api/object/ObjectAssert_extracting_with_String_Test.java
similarity index 60%
rename from src/test/java/org/assertj/core/api/object/ObjectAssert_extracting_Test.java
rename to src/test/java/org/assertj/core/api/object/ObjectAssert_extracting_with_String_Test.java
--- a/src/test/java/org/assertj/core/api/object/ObjectAssert_extracting_Test.java
+++ b/src/test/java/org/assertj/core/api/object/ObjectAssert_extracting_with_String_Test.java
@@ -30,10 +30,10 @@
 import org.junit.jupiter.api.Test;
 
 /**
- * Tests for <code>{@link ObjectAssert#extracting(String)}</code> and <code>{@link ObjectAssert#extracting(String[])}</code>.
+ * Tests for <code>{@link ObjectAssert#extracting(String)}</code>.
  */
-@DisplayName("ObjectAssert.extracting")
-class ObjectAssert_extracting_Test {
+@DisplayName("ObjectAssert extracting(String)")
+class ObjectAssert_extracting_with_String_Test {
 
   private Employee luke;
   private Employee leia;
@@ -43,42 +43,23 @@ void setup() {
     luke = new Employee(2L, new Name("Luke", "Skywalker"), 26);
     luke.setAttribute("side", "light");
     leia = new Employee(2L, new Name("Leia", "Skywalker"), 26);
+    luke.setRelation("brother", null);
     luke.setRelation("sister", leia);
     leia.setRelation("brother", luke);
   }
 
   @Test
   void should_allow_assertions_on_property_extracted_from_given_object_by_name() {
+    // WHEN/THEN
     assertThat(luke).extracting("id")
                     .isNotNull();
-    assertThat(luke).extracting("name.first")
-                    .isEqualTo("Luke");
-  }
-
-  @Test
-  void should_allow_assertions_on_array_of_properties_extracted_from_given_object_by_name() {
-    assertThat(luke).extracting("id", "name")
-                    .hasSize(2)
-                    .doesNotContainNull();
-    assertThat(luke).extracting("name.first", "name.last")
-                    .hasSize(2)
-                    .containsExactly("Luke", "Skywalker");
-  }
-
-  @Test
-  void should_allow_assertion_on_mixed_properties_or_fields_with_nested_map_values() {
-    assertThat(luke).extracting("id", "name.last", "attributes.side", "relations.sister", "relations.sister.relations.brother.id")
-                    .containsExactly(2L, "Skywalker", "light", leia, 2L);
   }
 
   @Test
-  void should_follow_map_get_behavior_for_unknown_key() {
-    assertThat(luke).extracting("attributes.unknown_key",
-                                "relations.sister",
-                                "relations.sista",
-                                "relations.sister.name.first",
-                                "relations.sista.name.first")
-                    .containsExactly(null, leia, null, "Leia", null);
+  void should_allow_assertions_on_nested_property_extracted_from_given_object_by_name() {
+    // WHEN/THEN
+    assertThat(luke).extracting("name.first")
+                    .isEqualTo("Luke");
   }
 
   @Test
@@ -89,15 +70,6 @@ void should_use_property_field_name_as_description_when_extracting_single_proper
     then(assertionError).hasMessageContaining("[Extracted: name.first]");
   }
 
-  @Test
-  void should_use_property_field_names_as_description_when_extracting_tuples_list() {
-    // WHEN
-    AssertionError assertionError = expectAssertionError(() -> assertThat(luke).extracting("name.first", "name.last")
-                                                                               .isEmpty());
-    // THEN
-    then(assertionError).hasMessageContaining("[Extracted: name.first, name.last]");
-  }
-
   @Test
   void should_keep_existing_description_if_set_when_extracting_single_property() {
     // WHEN
@@ -108,16 +80,6 @@ void should_keep_existing_description_if_set_when_extracting_single_property() {
     then(assertionError).hasMessageContaining("[check luke first name]");
   }
 
-  @Test
-  void should_keep_existing_description_if_set_when_extracting_tuples_list() {
-    // WHEN
-    AssertionError assertionError = expectAssertionError(() -> assertThat(luke).as("check luke first and last name")
-                                                                               .extracting("name.first", "name.last")
-                                                                               .isEmpty());
-    // THEN
-    then(assertionError).hasMessageContaining("[check luke first and last name]");
-  }
-
   @Test
   void should_allow_to_specify_type_comparator_after_using_extracting_with_single_parameter_on_object() {
     // GIVEN
@@ -127,23 +89,12 @@ void should_allow_to_specify_type_comparator_after_using_extracting_with_single_
       if (o1 instanceof BigDecimal) return BIG_DECIMAL_COMPARATOR.compare((BigDecimal) o1, (BigDecimal) o2);
       throw new IllegalStateException("only supported for BigDecimal");
     };
-    // THEN
+    // WHEN/THEN
     assertThat(obiwan).extracting("height")
                       .usingComparator(heightComparator)
                       .isEqualTo(new BigDecimal("1.82"));
   }
 
-  @Test
-  void should_allow_to_specify_type_comparator_after_using_extracting_with_multiple_parameters_on_object() {
-    // GIVEN
-    Person obiwan = new Person("Obi-Wan");
-    obiwan.setHeight(new BigDecimal("1.820"));
-    // THEN
-    assertThat(obiwan).extracting("name", "height")
-                      .usingComparatorForType(BIG_DECIMAL_COMPARATOR, BigDecimal.class)
-                      .containsExactly("Obi-Wan", new BigDecimal("1.82"));
-  }
-
   @Test
   void should_throw_IntrospectionError_if_given_field_name_cannot_be_read() {
     // WHEN
@@ -167,4 +118,5 @@ public void setHeight(BigDecimal height) {
       this.height = height;
     }
   }
+
 }
diff --git a/src/test/java/org/assertj/core/api/object/ObjectAssert_flatExtracting_Test.java b/src/test/java/org/assertj/core/api/object/ObjectAssert_flatExtracting_Test.java
deleted file mode 100644
--- a/src/test/java/org/assertj/core/api/object/ObjectAssert_flatExtracting_Test.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations under the License.
- *
- * Copyright 2012-2021 the original author or authors.
- */
-package org.assertj.core.api.object;
-
-import static java.util.Arrays.asList;
-import static org.assertj.core.api.Assertions.assertThat;
-
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-class ObjectAssert_flatExtracting_Test {
-
-  private Map<String, List<String>> mapOfList;
-
-  @BeforeEach
-  void beforeEachTest() {
-    mapOfList = new LinkedHashMap<>();
-    mapOfList.put("name", asList("Dave", "Jeff"));
-    mapOfList.put("job", asList("Plumber", "Builder"));
-    mapOfList.put("city", asList("Dover", "Boston", "Paris"));
-  }
-
-  @Test
-  void should_allow_assertions_on_flattened_values_extracted_from_given_map_keys() {
-    assertThat(mapOfList).flatExtracting("name", "job", "city")
-                         .containsExactly("Dave", "Jeff", "Plumber", "Builder", "Dover", "Boston", "Paris");
-    // order of values is the order of key then key values
-    assertThat(mapOfList).flatExtracting("city", "job", "name")
-                         .containsExactly("Dover", "Boston", "Paris", "Plumber", "Builder", "Dave", "Jeff");
-  }
-
-  @Test
-  void should_extract_null_from_unknown_key() {
-    assertThat(mapOfList).flatExtracting("name", "id", "city")
-                         .containsExactly("Dave", "Jeff", null, "Dover", "Boston", "Paris");
-    assertThat(mapOfList).flatExtracting("foo", "bar")
-                         .containsOnlyNulls();
-  }
-
-}
diff --git a/src/test/java/org/assertj/core/extractor/ByNameMultipleExtractorTest.java b/src/test/java/org/assertj/core/extractor/ByNameMultipleExtractorTest.java
--- a/src/test/java/org/assertj/core/extractor/ByNameMultipleExtractorTest.java
+++ b/src/test/java/org/assertj/core/extractor/ByNameMultipleExtractorTest.java
@@ -99,12 +99,24 @@ void should_throw_exception_when_no_object_is_given() {
   void should_extract_multiple_values_from_map_by_keys() {
     // GIVEN
     Employee luke = new Employee(2L, new Name("Luke"), 22);
-    Map<String, Employee> map = mapOf(entry("key1", YODA), entry("key2", luke));
-    ByNameMultipleExtractor underTest = new ByNameMultipleExtractor("key1", "key2", "bad key");
+    Map<String, Employee> map = mapOf(entry("key1", YODA), entry("key2", luke), entry("key3", null));
+    ByNameMultipleExtractor underTest = new ByNameMultipleExtractor("key1", "key2", "key3");
     // WHEN
     Tuple result = underTest.apply(map);
     // THEN
     then(result).isEqualTo(tuple(YODA, luke, null));
   }
 
+  @Test
+  void should_throw_error_with_map_when_non_existing_key_is_given() {
+    // GIVEN
+    Employee luke = new Employee(2L, new Name("Luke"), 22);
+    Map<String, Employee> map = mapOf(entry("key1", YODA), entry("key2", luke));
+    ByNameMultipleExtractor underTest = new ByNameMultipleExtractor("key1", "key2", "bad key");
+    // WHEN
+    Throwable thrown = catchThrowable(() -> underTest.apply(YODA));
+    // THEN
+    then(thrown).isInstanceOf(IntrospectionError.class);
+  }
+
 }
diff --git a/src/test/java/org/assertj/core/extractor/ByNameSingleExtractorTest.java b/src/test/java/org/assertj/core/extractor/ByNameSingleExtractorTest.java
--- a/src/test/java/org/assertj/core/extractor/ByNameSingleExtractorTest.java
+++ b/src/test/java/org/assertj/core/extractor/ByNameSingleExtractorTest.java
@@ -164,11 +164,22 @@ void should_extract_single_value_from_map_by_key() {
   }
 
   @Test
-  void should_extract_null_from_map_by_non_existing_key() {
+  void should_throw_error_from_map_by_non_existing_key() {
     // GIVEN
     Map<String, Employee> map = mapOf(entry("key", YODA));
     ByNameSingleExtractor underTest = new ByNameSingleExtractor("non-existing");
     // WHEN
+    Throwable thrown = catchThrowable(() -> underTest.apply(map));
+    // THEN
+    then(thrown).isInstanceOf(IntrospectionError.class);
+  }
+
+  @Test
+  void should_extract_null_from_map_by_key_with_null_value() {
+    // GIVEN
+    Map<String, Employee> map = mapOf(entry("key", null));
+    ByNameSingleExtractor underTest = new ByNameSingleExtractor("key");
+    // WHEN
     Object result = underTest.apply(map);
     // THEN
     then(result).isNull();
diff --git a/src/test/java/org/assertj/core/util/introspection/PropertyOrFieldSupport_getSimpleValue_with_Map_input_Test.java b/src/test/java/org/assertj/core/util/introspection/PropertyOrFieldSupport_getSimpleValue_with_Map_input_Test.java
--- a/src/test/java/org/assertj/core/util/introspection/PropertyOrFieldSupport_getSimpleValue_with_Map_input_Test.java
+++ b/src/test/java/org/assertj/core/util/introspection/PropertyOrFieldSupport_getSimpleValue_with_Map_input_Test.java
@@ -12,6 +12,7 @@
  */
 package org.assertj.core.util.introspection;
 
+import static org.assertj.core.api.Assertions.catchThrowable;
 import static org.assertj.core.api.BDDAssertions.then;
 
 import java.util.AbstractMap;
@@ -20,12 +21,14 @@
 
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
 
 /**
  * Tests for <code>{@link PropertyOrFieldSupport#getSimpleValue(String, Object)}</code> with {@code Map} input.
-  *
-  * @author Stefano Cordio
-  */
+ *
+ * @author Stefano Cordio
+ */
 @DisplayName("PropertyOrFieldSupport getSimpleValue(String, Map)")
 class PropertyOrFieldSupport_getSimpleValue_with_Map_input_Test {
 
@@ -52,22 +55,26 @@ void should_extract_field_value_even_if_map_key_matches_given_name() {
     then(value).isInstanceOf(Collection.class);
   }
 
-  @Test
-  void should_extract_map_value_when_no_property_or_field_matches_given_name() {
+  @ParameterizedTest
+  @CsvSource({
+      "key, value",
+      "key, ",
+  })
+  void should_extract_map_value_when_no_property_or_field_matches_given_name(String key, String expected) {
     // GIVEN
-    map.put("key", "value");
+    map.put(key, expected);
     // WHEN
-    Object value = underTest.getSimpleValue("key", map);
+    Object value = underTest.getSimpleValue(key, map);
     // THEN
-    then(value).isEqualTo("value");
+    then(value).isEqualTo(expected);
   }
 
   @Test
-  void should_extract_null_when_given_name_is_not_found() {
+  void should_fail_when_given_name_is_not_found() {
     // WHEN
-    Object value = underTest.getSimpleValue("unknown", map);
+    Throwable thrown = catchThrowable(() -> underTest.getSimpleValue("unknown", map));
     // THEN
-    then(value).isNull();
+    then(thrown).isInstanceOf(IntrospectionError.class);
   }
 
 }
diff --git a/src/test/java/org/assertj/core/util/introspection/PropertyOrFieldSupport_getValueOf_Test.java b/src/test/java/org/assertj/core/util/introspection/PropertyOrFieldSupport_getValueOf_Test.java
--- a/src/test/java/org/assertj/core/util/introspection/PropertyOrFieldSupport_getValueOf_Test.java
+++ b/src/test/java/org/assertj/core/util/introspection/PropertyOrFieldSupport_getValueOf_Test.java
@@ -14,8 +14,10 @@
 
 import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.assertj.core.api.Assertions.catchThrowable;
 import static org.assertj.core.api.BDDAssertions.then;
+import static org.assertj.core.util.AssertionsUtil.expectAssertionError;
 import static org.assertj.core.util.introspection.FieldSupport.EXTRACTION_OF_PUBLIC_FIELD_ONLY;
 
 import java.util.HashMap;
@@ -184,21 +186,26 @@ void should_throw_exception_if_no_object_is_given() {
   void should_extract_single_value_from_maps_by_key() {
     String key1 = "key1";
     String key2 = "key2";
+    String keyWithNullValue = "key with null";
     Map<String, Employee> map1 = new HashMap<>();
     map1.put(key1, yoda);
     Employee luke = new Employee(2L, new Name("Luke"), 22);
     map1.put(key2, luke);
+    map1.put(keyWithNullValue, null);
 
     Map<String, Employee> map2 = new HashMap<>();
     map2.put(key1, yoda);
     Employee han = new Employee(3L, new Name("Han"), 31);
     map2.put(key2, han);
+    map2.put(keyWithNullValue, null);
 
     List<Map<String, Employee>> maps = asList(map1, map2);
     assertThat(maps).extracting(key2).containsExactly(luke, han);
     assertThat(maps).extracting(key2, Employee.class).containsExactly(luke, han);
     assertThat(maps).extracting(key1).containsExactly(yoda, yoda);
-    assertThat(maps).extracting("bad key").containsExactly(null, null);
+    assertThat(maps).extracting(keyWithNullValue).containsExactly(null, null);
+
+    assertThatExceptionOfType(IntrospectionError.class).isThrownBy(() -> assertThat(maps).extracting("bad key"));
   }
 
   @Test