diff --git a/pom.xml b/pom.xml index 822711e..5ae2ab9 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -26,7 +27,7 @@ it.aboutbits archunit-toolbox - 1.0.0-RC1 + 1.1.0 diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullableCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullableCustomizer.java index 9ee0dde..749e71f 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullableCustomizer.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullableCustomizer.java @@ -5,13 +5,13 @@ import org.jspecify.annotations.NullMarked; import org.springdoc.core.customizers.OpenApiCustomizer; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedType; import java.util.ArrayList; import java.util.Map; @NullMarked public class NullableCustomizer implements OpenApiCustomizer { - public static final String NULLABLE_MARKER = "NULLABLE"; - @Override @SuppressWarnings("unchecked") public void customise(OpenAPI openApi) { @@ -23,14 +23,14 @@ public void customise(OpenAPI openApi) { var requiredProperties = new ArrayList(); if (((Schema) schema).getProperties() != null) { var properties = ((Schema) schema).getProperties(); - processProperties(properties, requiredProperties); + processProperties(schema.getName(), properties, requiredProperties); } if (schema.getAllOf() != null) { schema.getAllOf().forEach(allOfSchema -> { var allOfSchemaTyped = (Schema) allOfSchema; if (allOfSchemaTyped.getProperties() != null) { var properties = allOfSchemaTyped.getProperties(); - processProperties(properties, requiredProperties); + processProperties(schema.getName(), properties, requiredProperties); } }); } @@ -38,42 +38,115 @@ public void customise(OpenAPI openApi) { }); } - private static void processProperties(Map properties, ArrayList requiredProperties) { + @SuppressWarnings("rawtypes") + private static void processProperties( + String modelFqn, + Map properties, + ArrayList requiredProperties + ) { + var cls = loadClass(modelFqn); + if (cls == null) { + return; + } + properties.forEach((propertyName, property) -> { - var isNullable = isNullable(property); + var isNullable = isNullable(cls, propertyName); if (!isNullable) { requiredProperties.add(propertyName); } else { requiredProperties.remove(propertyName); } - if (property.getTitle() != null && property.getTitle().equals(NULLABLE_MARKER)) { - property.setTitle(null); - } - if (property.get$ref() != null) { - property.set$ref(property.get$ref().replace(NULLABLE_MARKER, "")); - } - if (property.getItems() != null && property.getItems().get$ref() != null) { - property.getItems().set$ref(property.getItems().get$ref().replace(NULLABLE_MARKER, "")); - } }); } - private static boolean isNullable(Schema property) { - if (property.getTitle() != null && property.getTitle().equals(NULLABLE_MARKER)) { - return true; + @org.jspecify.annotations.Nullable + private static Class loadClass(String fqn) { + try { + return Class.forName(fqn); + } catch (ClassNotFoundException _) { + // if this does not work, we probably have a parameterized type where the fqn is concatenated } - if (property.get$ref() != null && property.get$ref().endsWith(NULLABLE_MARKER)) { - return true; + var lastDotIndex = -1; + for (var i = 0; i <= fqn.length(); i++) { + if (i == fqn.length() || fqn.charAt(i) == '.') { + var fullPart = fqn.substring(lastDotIndex + 1, i); + if (!fullPart.isEmpty() && Character.isUpperCase(fullPart.charAt(0))) { + // Try the full part first + var baseFqn = fqn.substring(0, i); + try { + return Class.forName(baseFqn); + } catch (ClassNotFoundException _) { + } + + // Try stripping capitalized segments from the end of the part + // e.g., LabelAndDescriptionChoiceCom -> try LabelAndDescriptionChoice, then LabelAndDescription, etc. + for (var j = fullPart.length() - 1; j > 0; j--) { + if (Character.isUpperCase(fullPart.charAt(j))) { + var strippedPart = fullPart.substring(0, j); + var candidateFqn = fqn.substring(0, lastDotIndex + 1) + strippedPart; + try { + return Class.forName(candidateFqn); + } catch (ClassNotFoundException _) { + } + } + } + } + lastDotIndex = i; + } } + return null; + } + + private static boolean isNullable(Class cls, String propertyName) { + var currentClass = cls; + while (currentClass != null) { + try { + var field = currentClass.getDeclaredField(propertyName); + if (isNullable(field.getAnnotatedType(), field.getAnnotations())) { + return true; + } + } catch (NoSuchFieldException _) { + } + + for (var method : currentClass.getDeclaredMethods()) { + if (method.getName().equals(propertyName) + || method.getName().equals("get" + capitalize(propertyName)) + || method.getName().equals("is" + capitalize(propertyName))) { + if (isNullable(method.getAnnotatedReturnType(), method.getAnnotations())) { + return true; + } + } + } - if (property.getItems() != null && property.getItems().get$ref() != null && property.getItems() - .get$ref() - .endsWith(NULLABLE_MARKER)) { - return true; + currentClass = currentClass.getSuperclass(); } return false; } + + private static boolean isNullable( + AnnotatedType annotatedType, + Annotation[] annotations + ) { + for (var annotation : annotatedType.getAnnotations()) { + if (annotation.annotationType().getSimpleName().equals("Nullable")) { + return true; + } + } + for (var annotation : annotations) { + if (annotation.annotationType().getSimpleName().equals("Nullable")) { + return true; + } + } + return false; + } + + private static String capitalize(String str) { + if (str.isEmpty()) { + return str; + } + return str.substring(0, 1).toUpperCase() + str.substring(1); + } } diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullablePropertyCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullablePropertyCustomizer.java deleted file mode 100644 index f0418b0..0000000 --- a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullablePropertyCustomizer.java +++ /dev/null @@ -1,61 +0,0 @@ -package it.aboutbits.springboot.toolbox.swagger.customization.default_not_null; - -import io.swagger.v3.core.converter.AnnotatedType; -import io.swagger.v3.core.jackson.ModelResolver; -import io.swagger.v3.oas.models.media.Schema; -import org.jspecify.annotations.NullMarked; -import org.springdoc.core.customizers.PropertyCustomizer; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.Arrays; - -import static it.aboutbits.springboot.toolbox.swagger.customization.default_not_null.NullableCustomizer.NULLABLE_MARKER; - -/** - * We treat all properties as not-nullable by default unless we specify @Nullable - * This is the opposite of what swagger does by default. - *

- * This customizer adds a nullability maker. Later we process this to manipulate the required fields of a schema. - */ -@Component -@NullMarked -public class NullablePropertyCustomizer implements PropertyCustomizer { - static { - /* - We need this because the ModelResolver will only retain annotations with a matching name - that is contained in the list. - */ - - var list = new ArrayList<>(ModelResolver.NOT_NULL_ANNOTATIONS); - list.add("Nullable"); - - ModelResolver.NOT_NULL_ANNOTATIONS = list; - } - - @Override - public Schema customize(Schema property, AnnotatedType annotatedType) { - if (isAnnotatedAsNullable(annotatedType)) { - property.setTitle(NULLABLE_MARKER); - - // refs do not retain other properties - if (property.get$ref() != null) { - property.set$ref(property.get$ref() + NULLABLE_MARKER); - } - } - - return property; - } - - @SuppressWarnings("java:S1872") // Disabled: "Use an 'instanceof' comparison instead." We MUST match by name. - private static boolean isAnnotatedAsNullable(AnnotatedType annotatedType) { - if (annotatedType.getCtxAnnotations() == null) { - return false; - } - - return Arrays.stream(annotatedType.getCtxAnnotations()) - .anyMatch( - a -> "Nullable".equals(a.annotationType().getSimpleName()) - ); - } -} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/transformer/QueryTransformerTest.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/transformer/QueryTransformerTest.java index 76cb3b6..c50d452 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/persistence/transformer/QueryTransformerTest.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/transformer/QueryTransformerTest.java @@ -13,7 +13,8 @@ import org.springframework.beans.factory.annotation.Autowired; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @ApplicationTest @NullMarked @@ -141,12 +142,10 @@ void givenQueryWithMultipleResults_shouldFail() { var query = entityManager.createQuery("select q, 'xxx' from QueryTransformerTestModel q"); - assertThrows( - IllegalStateException.class, - () -> QueryTransformer - .of(entityManager, TestModelContainer.class) - .withQuery(query) - .asSingleResult() + assertThatIllegalStateException().isThrownBy(() -> QueryTransformer + .of(entityManager, TestModelContainer.class) + .withQuery(query) + .asSingleResult() ); } } @@ -172,11 +171,10 @@ void givenQueryWithOneResult_shouldPass() { void givenQueryWithOneResult_shouldFail() { var query = entityManager.createQuery("select q, 'xxx' from QueryTransformerTestModel q"); - assertThrows( - EntityNotFoundException.class, () -> QueryTransformer - .of(entityManager, TestModelContainer.class) - .withQuery(query) - .asSingleResultOrFail() + assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> QueryTransformer + .of(entityManager, TestModelContainer.class) + .withQuery(query) + .asSingleResultOrFail() ); } } @@ -221,12 +219,10 @@ void givenQuery_wrongTargetClass_shouldFail() { var query = entityManager.createQuery("select q, 'xxx' from QueryTransformerTestModel q"); - assertThrows( - TransformerRuntimeException.class, - () -> QueryTransformer - .of(entityManager, WrongContainer.class) - .withQuery(query) - .asList() + assertThatExceptionOfType(TransformerRuntimeException.class).isThrownBy(() -> QueryTransformer + .of(entityManager, WrongContainer.class) + .withQuery(query) + .asList() ); } } @@ -351,12 +347,10 @@ void givenVariousQueries_shouldPassReturningTheRightTotalCount() { void givenQueryWithSelectDistinct_shouldFail() { var query = entityManager.createQuery("select distinct q, 'xxx' from QueryTransformerTestModel q"); - assertThrows( - IllegalStateException.class, - () -> QueryTransformer - .of(entityManager, TestModelContainer.class) - .withQuery(query) - .asPage(1, 2) + assertThatIllegalStateException().isThrownBy(() -> QueryTransformer + .of(entityManager, TestModelContainer.class) + .withQuery(query) + .asPage(1, 2) ); } } diff --git a/src/test/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullableCustomizerTest.java b/src/test/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullableCustomizerTest.java new file mode 100644 index 0000000..8c840f6 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullableCustomizerTest.java @@ -0,0 +1,207 @@ +package it.aboutbits.springboot.toolbox.swagger.customization.default_not_null; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.StringSchema; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import static org.assertj.core.api.Assertions.assertThat; + +@NullUnmarked +class NullableCustomizerTest { + + public static class BaseClass { + @Nullable + private String baseField; + + public String getBaseField() { + return baseField; + } + } + + public static class SubClass extends BaseClass { + private String subField; + + public String getSubField() { + return subField; + } + } + + public static class MethodAnnotated { + private String annotatedGetter; + + @Nullable + public String getAnnotatedGetter() { + return annotatedGetter; + } + } + + public static class DirectMethodAnnotated { + private String directMethod; + + @Nullable + public String directMethod() { + return directMethod; + } + } + + @Test + void shouldFindFieldInSuperClass() { + // given + var customizer = new NullableCustomizer(); + var openApi = new OpenAPI(); + var components = new Components(); + + var subClassSchema = new Schema(); + subClassSchema.setName(SubClass.class.getName()); + subClassSchema.addProperty("baseField", new StringSchema()); + subClassSchema.addProperty("subField", new StringSchema()); + + components.addSchemas(SubClass.class.getName(), subClassSchema); + openApi.setComponents(components); + + // when + customizer.customise(openApi); + + // then + var required = subClassSchema.getRequired(); + + assertThat(required).as("subField should be required").contains("subField"); + assertThat(required).as("baseField should NOT be required").doesNotContain("baseField"); + } + + @Test + void shouldFindAnnotationOnGetter() { + // given + var customizer = new NullableCustomizer(); + var openApi = new OpenAPI(); + var components = new Components(); + + var schema = new Schema(); + schema.setName(MethodAnnotated.class.getName()); + schema.addProperty("annotatedGetter", new StringSchema()); + + components.addSchemas(MethodAnnotated.class.getName(), schema); + openApi.setComponents(components); + + // when + customizer.customise(openApi); + + // then + var required = schema.getRequired(); + assertThat(required).as("annotatedGetter should NOT be required").isNullOrEmpty(); + } + + @Test + void shouldFindAnnotationOnDirectMethod() { + // given + var customizer = new NullableCustomizer(); + var openApi = new OpenAPI(); + var components = new Components(); + + var schema = new Schema(); + schema.setName(DirectMethodAnnotated.class.getName()); + schema.addProperty("directMethod", new StringSchema()); + + components.addSchemas(DirectMethodAnnotated.class.getName(), schema); + openApi.setComponents(components); + + // when + customizer.customise(openApi); + + // then + var required = schema.getRequired(); + assertThat(required).as("directMethod should NOT be required").isNullOrEmpty(); + } + + @Test + void shouldHandleConcatenatedFqns() { + // given + var customizer = new NullableCustomizer(); + var openApi = new OpenAPI(); + var components = new Components(); + + var schema = new Schema(); + // Simulating the concatenated FQN pattern described in the issue + var concatenatedFqn = SubClass.class.getName() + "It.aboutbits.something"; + schema.setName(concatenatedFqn); + schema.addProperty("baseField", new StringSchema()); + + components.addSchemas(concatenatedFqn, schema); + openApi.setComponents(components); + + // when + customizer.customise(openApi); + + // then + var required = schema.getRequired(); + assertThat(required).as("baseField should NOT be required even with concatenated FQN").isNullOrEmpty(); + } + + @Test + void shouldNotThrowWhenFieldNotFound() { + // given + var customizer = new NullableCustomizer(); + var openApi = new OpenAPI(); + var components = new Components(); + + var schema = new Schema(); + schema.setName(SubClass.class.getName()); + schema.addProperty("nonExistent", new StringSchema()); + + components.addSchemas(SubClass.class.getName(), schema); + openApi.setComponents(components); + + // when + customizer.customise(openApi); + + // then + var required = schema.getRequired(); + assertThat(required).as("nonExistent field should be considered required if not found and not nullable") + .contains("nonExistent"); + } + + @Test + void shouldFindCustomNullableAnnotation() { + // given + var customizer = new NullableCustomizer(); + var openApi = new OpenAPI(); + var components = new Components(); + + var schema = new Schema(); + schema.setName(CustomNullableClass.class.getName()); + schema.addProperty("customNullableField", new StringSchema()); + + components.addSchemas(CustomNullableClass.class.getName(), schema); + openApi.setComponents(components); + + // when + customizer.customise(openApi); + + // then + var required = schema.getRequired(); + assertThat(required).as("customNullableField should NOT be required").isNullOrEmpty(); + } + + public static class Nest { + @Retention(RetentionPolicy.RUNTIME) + public @interface Nullable { + } + } + + public static class CustomNullableClass { + @Nest.Nullable + private String customNullableField; + + @SuppressWarnings("NullAway") + public String getCustomNullableField() { + return customNullableField; + } + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/util/CollectUtilTest.java b/src/test/java/it/aboutbits/springboot/toolbox/util/CollectUtilTest.java index 7cf8768..be813df 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/util/CollectUtilTest.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/util/CollectUtilTest.java @@ -14,8 +14,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; @NullMarked class CollectUtilTest { @@ -60,7 +58,7 @@ void shouldHandleEmptyCollectionWhenConvertingToSet() { var result = CollectUtil.collectToSet(emptyList, mapper); // then - assertTrue(result.isEmpty()); + assertThat(result).isEmpty(); } @Test @@ -74,8 +72,9 @@ void shouldRemoveDuplicatesWhenConvertingCollectionToSet() { var result = CollectUtil.collectToSet(numbersWithDuplicates, mapper); // then - assertThat(result).containsExactlyInAnyOrder("1", "2", "3"); - assertEquals(3, result.size()); + assertThat(result) + .containsExactlyInAnyOrder("1", "2", "3") + .hasSize(3); } @Test @@ -117,7 +116,7 @@ void shouldHandleEmptyStreamableWhenConvertingToSet() { var result = CollectUtil.collectToSet(emptyStreamable, mapper); // then - assertTrue(result.isEmpty()); + assertThat(result).isEmpty(); } @Test @@ -145,7 +144,7 @@ void shouldHandleEmptyStreamWhenConvertingToSet() { var result = CollectUtil.collectToSet(emptyStream, mapper); // then - assertTrue(result.isEmpty()); + assertThat(result).isEmpty(); } @Test @@ -159,8 +158,9 @@ void shouldRemoveDuplicatesWhenConvertingStreamToSet() { var result = CollectUtil.collectToSet(numbersWithDuplicates, mapper); // then - assertThat(result).containsExactlyInAnyOrder("1", "2", "3"); - assertEquals(3, result.size()); + assertThat(result) + .containsExactlyInAnyOrder("1", "2", "3") + .hasSize(3); } } @@ -191,7 +191,7 @@ void shouldHandleEmptyCollectionWhenConvertingToList() { var result = CollectUtil.collectToList(emptyList, mapper); // then - assertTrue(result.isEmpty()); + assertThat(result).isEmpty(); } @Test @@ -205,8 +205,9 @@ void shouldPreserveDuplicatesWhenConvertingCollectionToList() { var result = CollectUtil.collectToList(numbersWithDuplicates, mapper); // then - assertThat(result).containsExactly("1", "2", "2", "3", "3", "3"); - assertEquals(6, result.size()); + assertThat(result) + .containsExactly("1", "2", "2", "3", "3", "3") + .hasSize(6); } @Test @@ -234,7 +235,7 @@ void shouldHandleEmptyStreamableWhenConvertingToList() { var result = CollectUtil.collectToList(emptyStreamable, mapper); // then - assertTrue(result.isEmpty()); + assertThat(result).isEmpty(); } @Test @@ -262,7 +263,7 @@ void shouldHandleEmptyStreamWhenConvertingToList() { var result = CollectUtil.collectToList(emptyStream, mapper); // then - assertTrue(result.isEmpty()); + assertThat(result).isEmpty(); } @Test @@ -276,8 +277,9 @@ void shouldPreserveDuplicatesWhenConvertingStreamToList() { var result = CollectUtil.collectToList(numbersWithDuplicates, mapper); // then - assertThat(result).containsExactly("1", "2", "2", "3", "3", "3"); - assertEquals(6, result.size()); + assertThat(result) + .containsExactly("1", "2", "2", "3", "3", "3") + .hasSize(6); } } @@ -310,7 +312,7 @@ void shouldHandleEmptyCollectionWhenConvertingToStream() { var result = resultStream.toList(); // then - assertTrue(result.isEmpty()); + assertThat(result).isEmpty(); } @Test @@ -340,7 +342,7 @@ void shouldHandleEmptyStreamableWhenConvertingToStream() { var result = resultStream.toList(); // then - assertTrue(result.isEmpty()); + assertThat(result).isEmpty(); } @Test @@ -370,7 +372,7 @@ void shouldHandleEmptyStreamWhenConvertingToStream() { var result = resultStream.toList(); // then - assertTrue(result.isEmpty()); + assertThat(result).isEmpty(); } } diff --git a/src/test/java/it/aboutbits/springboot/toolbox/util/FilterUtilTest.java b/src/test/java/it/aboutbits/springboot/toolbox/util/FilterUtilTest.java index 4272b64..e12e04b 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/util/FilterUtilTest.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/util/FilterUtilTest.java @@ -11,7 +11,6 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; @NullMarked class FilterUtilTest { @@ -40,7 +39,7 @@ void shouldHandleEmptyCollectionWhenFilteringToSet() { var result = FilterUtil.filterToSet(emptyList, n -> n > 2); // then - assertTrue(result.isEmpty()); + assertThat(result).isEmpty(); } @Test @@ -53,8 +52,9 @@ void shouldRemoveDuplicatesWhenFilteringCollectionToSet() { var result = FilterUtil.filterToSet(numbersWithDuplicates, n -> n >= 2); // then - assertThat(result).containsExactlyInAnyOrder(2, 3); - assertThat(result).hasSize(2); + assertThat(result) + .hasSize(2) + .containsExactlyInAnyOrder(2, 3); } @Test @@ -80,7 +80,7 @@ void shouldHandleEmptyStreamableWhenFilteringToSet() { var result = FilterUtil.filterToSet(emptyStreamable, n -> n > 2); // then - assertTrue(result.isEmpty()); + assertThat(result).isEmpty(); } @Test @@ -93,8 +93,9 @@ void shouldRemoveDuplicatesWhenFilteringStreamToSet() { var result = FilterUtil.filterToSet(numbersWithDuplicates, n -> n >= 2); // then - assertThat(result).containsExactlyInAnyOrder(2, 3); - assertThat(result).hasSize(2); + assertThat(result) + .hasSize(2) + .containsExactlyInAnyOrder(2, 3); } } @@ -123,7 +124,7 @@ void shouldHandleEmptyCollectionWhenFilteringToList() { var result = FilterUtil.filterToList(emptyList, n -> n > 2); // then - assertTrue(result.isEmpty()); + assertThat(result).isEmpty(); } @Test @@ -136,8 +137,9 @@ void shouldPreserveDuplicatesWhenFilteringCollectionToList() { var result = FilterUtil.filterToList(numbersWithDuplicates, n -> n >= 2); // then - assertThat(result).containsExactly(2, 2, 3, 3, 3); - assertThat(result).hasSize(5); + assertThat(result) + .hasSize(5) + .containsExactly(2, 2, 3, 3, 3); } @Test @@ -163,7 +165,7 @@ void shouldHandleEmptyStreamableWhenFilteringToList() { var result = FilterUtil.filterToList(emptyStreamable, n -> n > 2); // then - assertTrue(result.isEmpty()); + assertThat(result).isEmpty(); } @Test @@ -176,8 +178,9 @@ void shouldPreserveDuplicatesWhenFilteringStreamToList() { var result = FilterUtil.filterToList(numbersWithDuplicates, n -> n >= 2); // then - assertThat(result).containsExactly(2, 2, 3, 3, 3); - assertThat(result).hasSize(5); + assertThat(result) + .hasSize(5) + .containsExactly(2, 2, 3, 3, 3); } } @@ -207,7 +210,7 @@ void shouldHandleEmptyCollectionWhenFilteringToStream() { var resultStream = FilterUtil.filterToStream(emptyList, n -> n > 2); // then - assertTrue(resultStream.toList().isEmpty()); + assertThat(resultStream.toList()).isEmpty(); } @Test @@ -234,7 +237,7 @@ void shouldHandleEmptyStreamableWhenFilteringToStream() { var resultStream = FilterUtil.filterToStream(emptyStreamable, n -> n > 2); // then - assertTrue(resultStream.toList().isEmpty()); + assertThat(resultStream.toList()).isEmpty(); } @Test @@ -261,7 +264,7 @@ void shouldHandleEmptyStreamWhenFilteringToStream() { var resultStream = FilterUtil.filterToStream(emptyStream, n -> n > 2); // then - assertTrue(resultStream.toList().isEmpty()); + assertThat(resultStream.toList()).isEmpty(); } } }