Skip to content

Commit

Permalink
Add 'extracting' with single parameter to AbstractObjectAssert and Ab…
Browse files Browse the repository at this point in the history
…stractMapAssert. Fixes #1469.

This improves developer experience by returning an ObjectAssert in place of a ListAssert which was not great to check a single value.
  • Loading branch information
scordio authored and joel-costigliola committed Apr 14, 2019
1 parent 27fa6b1 commit 16e1667
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 62 deletions.
49 changes: 38 additions & 11 deletions src/main/java/org/assertj/core/api/AbstractMapAssert.java
Expand Up @@ -575,7 +575,7 @@ public SELF containsAnyOf(@SuppressWarnings("unchecked") Map.Entry<? extends K,
* @throws AssertionError if the actual map does not contain the given entries.
*/
public SELF containsAllEntriesOf(Map<? extends K, ? extends V> other) {
Map.Entry<? extends K, ? extends V>[] entries = other.entrySet().toArray(new Map.Entry[other.size()]);
Map.Entry<? extends K, ? extends V>[] entries = other.entrySet().toArray(new Map.Entry[0]);
maps.assertContains(info, actual, entries);
return myself;
}
Expand Down Expand Up @@ -618,7 +618,7 @@ public SELF containsAllEntriesOf(Map<? extends K, ? extends V> other) {
* @since 3.12.0
*/
public SELF containsExactlyEntriesOf(Map<? extends K, ? extends V> map) {
Map.Entry<? extends K, ? extends V>[] entries = map.entrySet().toArray(new Map.Entry[map.size()]);
Map.Entry<? extends K, ? extends V>[] entries = map.entrySet().toArray(new Map.Entry[0]);
return containsExactly(entries);
}

Expand Down Expand Up @@ -989,7 +989,6 @@ public SELF containsKey(K key) {
* @throws AssertionError if the actual map does not contain the given key.
* @throws IllegalArgumentException if the given argument is an empty array.
*/

public SELF containsKeys(@SuppressWarnings("unchecked") K... keys) {
maps.assertContainsKeys(info, actual, keys);
return myself;
Expand Down Expand Up @@ -1155,7 +1154,6 @@ public SELF containsValue(V value) {
* @throws AssertionError if the actual map is {@code null}.
* @throws AssertionError if the actual map does not contain the given values.
*/

public SELF containsValues(@SuppressWarnings("unchecked") V... values) {
maps.assertContainsValues(info, actual, values);
return myself;
Expand Down Expand Up @@ -1515,28 +1513,57 @@ public AbstractMapSizeAssert<SELF, ACTUAL, K, V> size() {
* @return a new assertion object whose object under test is the array containing the extracted map values
*/
@CheckReturnValue
public AbstractListAssert<?, List<? extends Object>, Object, ObjectAssert<Object>> extracting(Object... keys) {
public AbstractListAssert<?, List<?>, Object, ObjectAssert<Object>> extracting(Object... keys) {
isNotNull();
List<Object> extractedValues = Stream.of(keys).map(actual::get).collect(Collectors.toList());
String extractedPropertiesOrFieldsDescription = extractedDescriptionOf(keys);
String description = mostRelevantDescription(info.description(), extractedPropertiesOrFieldsDescription);
return newListAssertInstance(extractedValues).as(description);
}

/**
* Extract the value of given key from the map under test, the extracted value becoming the new object under test.
* <p>
* For example, if you specify "id" key, then the object under test will be the map value for this key.
* <p>
* If a given key is not present in the map under test, a null value is extracted.
* <p>
* Example:
* <pre><code class='java'> Map&lt;String, Object&gt; map = new HashMap&lt;&gt;();
* map.put("name", "kawhi");
*
* assertThat(map).extracting("name")
* .isEqualTo("kawhi");</code></pre>
* <p>
* Nested keys are not yet supported, passing "name.first" won't get a value for "name" and then try to extract
* "first" from the previously extracted value, instead it will simply look for a value under "name.first" key.
*
* @param key the key used to get value from the map under test
* @return a new {@link ObjectAssert} instance whose object under test is the extracted map value
*/
@CheckReturnValue
public AbstractObjectAssert<?, ?> extracting(Object key) {
isNotNull();
Object extractedValue = actual.get(key);
String extractedPropertyOrFieldDescription = extractedDescriptionOf(key);
String description = mostRelevantDescription(info.description(), extractedPropertyOrFieldDescription);
return newObjectAssert(extractedValue).as(description);
}

/**
* Use the given {@link Function} to extract a value from the {@link Map}'s entries.
* The extracted values are stored in a new list becoming the object under test.
* <p>
* Let's take a look at an example to make things clearer :
* <pre><code class='java'> // Build a Map that associates family roles and name of the Simpson familly
* Map&lt;String, CartoonCharacter&gt; characters = new HashMap&lt;&gt;();
* characters.put(&quot;dad&quot;, new CartoonCharacter(&quot;Omer&quot;));
* characters.put(&quot;dad&quot;, new CartoonCharacter(&quot;Homer&quot;));
* characters.put(&quot;mom&quot;, new CartoonCharacter(&quot;Marge&quot;));
* characters.put(&quot;girl&quot;, new CartoonCharacter(&quot;Lisa&quot;));
* characters.put(&quot;boy&quot;, new CartoonCharacter(&quot;Bart&quot;));
*
* assertThat(characters).extractingFromEntries(e -&gt; e.getValue().getName())
* .containsOnly(&quot;Omer&quot;, &quot;Marge&quot;, &quot;Lisa&quot;, &quot;Bart&quot;);</code></pre>
* .containsOnly(&quot;Homer&quot;, &quot;Marge&quot;, &quot;Lisa&quot;, &quot;Bart&quot;);</code></pre>
*
* @param extractor the extractor function to extract a value from an entry of the Map under test.
* @return a new assertion object whose object under test is the list of values extracted
Expand All @@ -1546,7 +1573,7 @@ public AbstractListAssert<?, List<? extends Object>, Object, ObjectAssert<Object
public AbstractListAssert<?, List<?>, Object, ObjectAssert<Object>> extractingFromEntries(Function<? super Map.Entry<K, V>, Object> extractor) {
isNotNull();
List<Object> extractedObjects = actual.entrySet().stream()
.map(extractor::apply)
.map(extractor)
.collect(toList());
return newListAssertInstance(extractedObjects).as(info.description());
}
Expand All @@ -1567,13 +1594,13 @@ public AbstractListAssert<?, List<?>, Object, ObjectAssert<Object>> extractingFr
* Let's take a look at an example to make things clearer :
* <pre><code class='java'> // Build a Map that associates family roles and name of the Simpson familly
* Map&lt;String, CartoonCharacter&gt; characters = new HashMap&lt;&gt;();
* characters.put(&quot;dad&quot;, new CartoonCharacter(&quot;Omer&quot;));
* characters.put(&quot;dad&quot;, new CartoonCharacter(&quot;Homer&quot;));
* characters.put(&quot;mom&quot;, new CartoonCharacter(&quot;Marge&quot;));
* characters.put(&quot;girl&quot;, new CartoonCharacter(&quot;Lisa&quot;));
* characters.put(&quot;boy&quot;, new CartoonCharacter(&quot;Bart&quot;));
*
* assertThat(characters).extractingFromEntries(e -&gt; e.getKey(), e -&gt; e.getValue().getName())
* .containsOnly(tuple(&quot;dad&quot;, &quot;Omer&quot;),
* .containsOnly(tuple(&quot;dad&quot;, &quot;Homer&quot;),
* tuple(&quot;mom&quot;, &quot;Marge&quot;),
* tuple(&quot;girl&quot;, &quot;Lisa&quot;),
* tuple(&quot;boy&quot;, &quot;Bart&quot;));</code></pre>
Expand Down Expand Up @@ -1645,7 +1672,7 @@ public AbstractListAssert<?, List<? extends Tuple>, Tuple, ObjectAssert<Tuple>>
* @return a new assertion object whose object under test is the array containing the extracted flattened map values
*/
@CheckReturnValue
public AbstractListAssert<?, List<? extends Object>, Object, ObjectAssert<Object>> flatExtracting(String... keys) {
public AbstractListAssert<?, List<?>, Object, ObjectAssert<Object>> flatExtracting(String... keys) {
Tuple values = byName(keys).apply(actual);
List<Object> valuesFlattened = flatten(values.toList());
String extractedPropertiesOrFieldsDescription = extractedDescriptionOf(keys);
Expand Down
43 changes: 40 additions & 3 deletions src/main/java/org/assertj/core/api/AbstractObjectAssert.java
Expand Up @@ -607,13 +607,50 @@ public SELF hasFieldOrPropertyWithValue(String name, Object value) {
* @throws IntrospectionError if one of the given name does not match a field or property
*/
@CheckReturnValue
public AbstractListAssert<?, List<? extends Object>, Object, ObjectAssert<Object>> extracting(String... propertiesOrFields) {
public AbstractListAssert<?, List<?>, Object, ObjectAssert<Object>> extracting(String... propertiesOrFields) {
Tuple values = byName(propertiesOrFields).apply(actual);
String extractedPropertiesOrFieldsDescription = extractedDescriptionOf(propertiesOrFields);
String description = mostRelevantDescription(info.description(), extractedPropertiesOrFieldsDescription);
return newListAssertInstance(values.toList()).as(description);
}

/**
* Extracts the value of given field/property from the object under test, the extracted value becoming the new object under test.
* <p>
* Nested field/property is supported, specifying "adress.street.number" is equivalent to get the value
* corresponding to actual.getAdress().getStreet().getNumber()
* <p>
* Private field can be extracted unless you call {@link Assertions#setAllowExtractingPrivateFields(boolean) Assertions.setAllowExtractingPrivateFields(false)}.
* <p>
* Note that since the value is extracted as an Object, only Object assertions can be chained after extracting.
* <p>
* Example:
* <pre><code class='java'> // Create frodo, setting its name, age and Race (Race having a name property)
* TolkienCharacter frodo = new TolkienCharacter(&quot;Frodo&quot;, 33, HOBBIT);
*
* // let's extract and verify Frodo's name:
* assertThat(frodo).extracting(&quot;name&quot;)
* .isEqualTo(&quot;Frodo&quot;);
*
* // The extracted value being a String, we would like to use String assertions but we can't due to Java generics limitations.
* // The following assertion does NOT compile:
* assertThat(frodo).extracting(&quot;name&quot;)
* .startsWith(&quot;Fro&quot;);</code></pre>
*
* A property with the given name is looked for first, if it doesn't exist then a field with the given name is looked
* for, if the field is not accessible (i.e. does not exist) an {@link IntrospectionError} is thrown.
*
* @param propertyOrField the property/field to extract from the initial object under test
* @return a new {@link ObjectAssert} instance whose object under test is the extracted property/field values
* @throws IntrospectionError if one of the given name does not match a field or property
*/
public AbstractObjectAssert<?, ?> extracting(String propertyOrField) {
Object value = byName(propertyOrField).apply(actual);
String extractedPropertyOrFieldDescription = extractedDescriptionOf(propertyOrField);
String description = mostRelevantDescription(info.description(), extractedPropertyOrFieldDescription);
return newObjectAssert(value).as(description);
}

/**
* Uses the given {@link Function}s to extract the values from the object under test into a list, this new list becoming
* the object under test.
Expand All @@ -637,7 +674,7 @@ public AbstractListAssert<?, List<? extends Object>, Object, ObjectAssert<Object
* @return a new assertion object whose object under test is the list containing the extracted values
*/
@CheckReturnValue
public AbstractListAssert<?, List<? extends Object>, Object, ObjectAssert<Object>> extracting(@SuppressWarnings("unchecked") Function<? super ACTUAL, Object>... extractors) {
public AbstractListAssert<?, List<?>, Object, ObjectAssert<Object>> extracting(@SuppressWarnings("unchecked") Function<? super ACTUAL, ?>... extractors) {
List<Object> values = Stream.of(extractors)
.map(extractor -> extractor.apply(actual))
.collect(toList());
Expand Down Expand Up @@ -667,7 +704,7 @@ public AbstractListAssert<?, List<? extends Object>, Object, ObjectAssert<Object
*
* @since 3.11.0
*/
public AbstractObjectAssert<?, ?> extracting(Function<? super ACTUAL, ? extends Object> extractor) {
public AbstractObjectAssert<?, ?> extracting(Function<? super ACTUAL, ?> extractor) {
requireNonNull(extractor, "The given java.util.function.Function extractor must not be null");
Object extractedValue = extractor.apply(actual);
return newObjectAssert(extractedValue).withAssertionState(myself);
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/assertj/core/api/MapAssert.java
Expand Up @@ -96,7 +96,7 @@ public final MapAssert<KEY, VALUE> doesNotContain(Map.Entry<? extends KEY, ? ext

@SafeVarargs
@Override
public final AbstractListAssert<?, List<?>, Object, ObjectAssert<Object>> extracting(Function<? super Map<KEY, VALUE>, Object>... extractors) {
public final AbstractListAssert<?, List<?>, Object, ObjectAssert<Object>> extracting(Function<? super Map<KEY, VALUE>, ?>... extractors) {
return super.extracting(extractors);
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/assertj/core/api/ObjectAssert.java
Expand Up @@ -43,7 +43,7 @@ public ObjectAssert(AtomicReference<ACTUAL> actual) {
@Override
@CheckReturnValue
@SafeVarargs
public final AbstractListAssert<?, List<? extends Object>, Object, ObjectAssert<Object>> extracting(Function<? super ACTUAL, Object>... extractors) {
public final AbstractListAssert<?, List<?>, Object, ObjectAssert<Object>> extracting(Function<? super ACTUAL, ?>... extractors) {
return super.extracting(extractors);
}

Expand Down
25 changes: 20 additions & 5 deletions src/test/java/org/assertj/core/api/BDDSoftAssertionsTest.java
Expand Up @@ -14,6 +14,7 @@

import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -270,7 +271,7 @@ public String toString() {
throw new Exception("something was wrong");
}).hasMessage("something was good");
softly.then(mapOf(MapEntry.entry("54", "55"))).contains(MapEntry.entry("1", "2"));
softly.then(LocalTime.of(12, 00)).isEqualTo(LocalTime.of(13, 00));
softly.then(LocalTime.of(12, 0)).isEqualTo(LocalTime.of(13, 0));
softly.then(OffsetTime.of(12, 0, 0, 0, ZoneOffset.UTC))
.isEqualTo(OffsetTime.of(13, 0, 0, 0, ZoneOffset.UTC));
softly.then(Optional.of("not empty")).isEqualTo("empty");
Expand Down Expand Up @@ -733,7 +734,7 @@ public void should_collect_all_errors_when_using_flatMap() {
@Test
public void should_propagate_AssertionError_from_nested_proxied_calls() {
// the nested proxied call to isNotEmpty() throw an Assertion error that must be propagated to the caller.
softly.then(asList()).first();
softly.then(emptyList()).first();
// nested proxied call to throwAssertionError when checking that is optional is present
softly.then(Optional.empty()).contains("Foo");
// nested proxied call to isNotNull
Expand Down Expand Up @@ -910,7 +911,7 @@ public void should_fix_bug_1146() {
.containsExactly("1", "2");
softly.then(numbers)
.extracting("one")
.containsExactly("1");
.isEqualTo("1");
}
}

Expand Down Expand Up @@ -1476,14 +1477,20 @@ public void object_soft_assertions_should_report_errors_on_final_methods_and_met
.overridingErrorMessage("error message")
.extracting(Name::getFirst)
.isEqualTo("Jack");
softly.then(name)
.as("extracting(first)")
.overridingErrorMessage("error message")
.extracting("first")
.isEqualTo("Jack");
// THEN
List<Throwable> errorsCollected = softly.errorsCollected();
assertThat(errorsCollected).hasSize(5);
assertThat(errorsCollected).hasSize(6);
assertThat(errorsCollected.get(0)).hasMessageContaining("gandalf");
assertThat(errorsCollected.get(1)).hasMessageContaining("frodo");
assertThat(errorsCollected.get(2)).hasMessageContaining("123");
assertThat(errorsCollected.get(3)).hasMessageContaining("\"1\", \"2\"");
assertThat(errorsCollected.get(4)).hasMessage("[extracting(Name::getFirst)] error message");
assertThat(errorsCollected.get(5)).hasMessage("[extracting(first)] error message");
}

// the test would fail if any method was not proxyable as the assertion error would not be softly caught
Expand All @@ -1508,10 +1515,17 @@ public void map_soft_assertions_should_report_errors_on_final_methods_and_method
.contains("Unexpected", "Builder", "Dover", "Boston", "Paris", 1, 2, 3);
Map<String, String> exactlyEntriesMap = mapOf(entry("kl", "KL"), entry("mn", "MN"));
softly.then(map).containsExactlyEntriesOf(exactlyEntriesMap);
softly.then(map)
.as("extracting(\"a\")")
.overridingErrorMessage("error message")
// convert to Object otherwise will use extracting(String) in AbstractObjectAssert
.extracting((Object) "a")
.isEqualTo("456");

// softly.then(map).size().isGreaterThan(1000); not yet supported
// THEN
List<Throwable> errors = softly.errorsCollected();
assertThat(errors).hasSize(13);
assertThat(errors).hasSize(14);
assertThat(errors.get(0)).hasMessageContaining("MapEntry[key=\"abc\", value=\"ABC\"]");
assertThat(errors.get(1)).hasMessageContaining("empty");
assertThat(errors.get(2)).hasMessageContaining("gh")
Expand All @@ -1526,6 +1540,7 @@ public void map_soft_assertions_should_report_errors_on_final_methods_and_method
assertThat(errors.get(10)).hasMessageContaining("456");
assertThat(errors.get(11)).hasMessageContaining("Unexpected");
assertThat(errors.get(12)).hasMessageContaining("\"a\"=\"1\"");
assertThat(errors.get(13)).hasMessage("[extracting(\"a\")] error message");
}

@Test
Expand Down

0 comments on commit 16e1667

Please sign in to comment.