All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.kiwiproject.test.validation.ParameterizedValidationTestHelper Maven / Gradle / Ivy

There is a newer version: 3.7.0
Show newest version
package org.kiwiproject.test.validation;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Lists.newArrayList;
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull;
import static org.kiwiproject.stream.IntStreams.indicesOf;
import static org.kiwiproject.test.junit.ParameterizedTests.inputs;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import org.assertj.core.api.SoftAssertions;

import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;

/**
 * Test helper for running parameterized validation tests. Uses AssertJ's {@link SoftAssertions} to allow tests
 * to gather all assertion failures across a range of inputs, rather than failing on the first assertion failure.
 * 

* NOTE: When writing new tests, consider instead using JUnit Jupiter's {@code @ParameterizedTest}. * This class was created years before JUnit Jupiter existed, and we still have code that uses it. Depending * on the specific context, the methods here might end up creating very readable tests. */ public class ParameterizedValidationTestHelper { private static final String A_STRING_VALUE = "a value"; private final Validator validator; private final SoftAssertions softly; /** * Create a new helper with a default {@link Validator} instance. * * @param softly the {@link SoftAssertions} instance to use */ public ParameterizedValidationTestHelper(SoftAssertions softly) { this(softly, ValidationTestHelper.newValidator()); } /** * Create a new helper. * * @param softly the {@link SoftAssertions} instance to use * @param validator the {@link Validator} to use when validating objects */ public ParameterizedValidationTestHelper(SoftAssertions softly, Validator validator) { this.validator = validator; this.softly = softly; } /** * Creates a list of the expected number of constraint violations. This is mainly for readability in tests. * * @param values the values to use as the number of expected constraint violations in a parameterized test * @return a mutable list containing the given values */ public static List expectedViolations(Integer... values) { return newArrayList(values); } /** * Creates a list of String lists for expected constraint violation messages. This is mainly for readability in * tests. * * @param values the values to use as the expected constraint violation messages in a parameterized test * @return a mutable list containing the given values */ @SafeVarargs public static List> expectedMessagesLists(List... values) { return newArrayList(values); } /** * Creates a list of expected constraint violation messages. This is mainly for readability in tests. * * @param values the values to use as the expected constraint violation messages in a parameterized test * @return a mutable list containing the given values */ public static List expectedMessages(String... values) { return newArrayList(values); } /** * Creates an empty list that can be used to indicate there are no expected constraint violation messages. * This is mainly for readability in tests. * * @return an immutable empty list */ public static List noExpectedMessages() { return Collections.emptyList(); } /** * Creates an array of classes that represent validation groups. This is mainly for readability in tests. * * @param groups the validation group classes * @return an array of the group {@link Class} objects * @implNote Aside from the readability aspect, this is a nifty little "trick" to "convert" a vararg into an * array of objects of the same type */ public static Class[] validationGroups(Class... groups) { return groups; } /** * Given a list of input values, supply each one to {@code mutator}, and softly assert that the number of * constraint violations matches the numbers in {@code expectedViolations}. The {@code inputValues} and * {@code expectedViolations} are expected to be the same length and to match at each index, i.e. * that {@code expectedViolations[N]} is the expected number of errors when applying {@code inputValues[N]} * as the input. *

* Example: Suppose you have a {@code Person} class that has a {@code lastName} property with * {@code @NotBlank} and {@code @Length(min = 2, max = 100)} validation annotations. You can then write a * test like this: *

     * {@literal @Test}
     *  void shouldValidatePersonLastName(SoftAssertions softly) {
     *      var p = new Person();
     *      List<String> inputs = inputs("Smith", "Ng", "X", "", " ", null);
     *      List<Integer> expected = expectedViolations(0, 0, 1, 2, 2, 1);
     *      var helper = new ParameterizedValidationTestHelper(softly);
     *      helper.assertPropertyViolationCounts("lastName", inputs, expected, p, p::setLastName);
     *  }
     * 
* * @param propertyName the property to validate * @param inputValues the inputs * @param expectedViolations the expected number of violations corresponding to the inputs * @param object the object to validate * @param mutator the mutator function, e.g. a setter method * @param groups the group or list of groups targeted for validation (defaults to Default) * @param the input type * @param the object type */ public void assertPropertyViolationCounts(String propertyName, List inputValues, List expectedViolations, U object, Consumer mutator, Class... groups) { checkArgumentNotNull(propertyName); checkInputAndExpectedValues(inputValues, expectedViolations, "expectedViolations"); indicesOf(inputValues).forEach(index -> { var input = inputValues.get(index); mutator.accept(input); var violations = validator.validateProperty(object, propertyName, groups); softly.assertThat(violations) .describedAs("input: [%s]", input) .hasSize(expectedViolations.get(index)); }); } /** * Given a list of input values, supply each one to {@code mutator}, and softly assert that the constraint * violation messages match those in {@code expectedViolationMessages}. The {@code inputValues} and * {@code expectedViolations} are expected to be the same length and to match at each index, i.e. * that {@code expectedViolations[N]} contains the expected violation messages when applying * {@code inputValues[N]} as the input. *

* Example: Suppose you have a {@code Person} class that has a {@code lastName} property with * {@code @NotBlank} and {@code @Length(min = 2, max = 100)} validation annotations. You can then write a * test like this: *

     * {@literal @Test}
     *  void shouldValidatePersonLastName(SoftAssertions softly) {
     *      var p = new Person();
     *      List<String> inputs = inputs("Smith", "Ng", "X", "", " ", null);
     *      List<List<String>> expected = expectedMessagesLists(
     *          noExpectedMessages(),
     *          noExpectedMessages(),
     *          expectedMessages("length must be between 2 and 100"),
     *          expectedMessages("must not be blank", "length must be between 2 and 100"),
     *          expectedMessages("must not be blank", "length must be between 2 and 100"),
     *          expectedMessages("must not be blank")
     *      );
     *      var helper = new ParameterizedValidationTestHelper(softly);
     *      helper.assertPropertyViolationCounts("lastName", inputs, expected, p, p::setLastName);
     *  }
     * 
* * @param propertyName the property to validate * @param inputValues the inputs * @param expectedViolationMessages the expected violation error messages corresponding to the inputs * @param object the object to validate * @param mutator the mutator function, e.g. a setter method * @param groups the group or list of groups targeted for validation (defaults to Default) * @param the input type * @param the object type */ public void assertPropertyViolationMessages(String propertyName, List inputValues, List> expectedViolationMessages, U object, Consumer mutator, Class... groups) { checkArgumentNotNull(propertyName); checkInputAndExpectedValues(inputValues, expectedViolationMessages, "expectedViolationMessages"); indicesOf(inputValues).forEach(index -> { var input = inputValues.get(index); mutator.accept(input); var violations = validator.validateProperty(object, propertyName, groups); softly.assertThat(violations) .extracting(ConstraintViolation::getMessage) .describedAs("input: [%s]", input) .hasSameElementsAs(expectedViolationMessages.get(index)); }); } private static void checkInputAndExpectedValues(List inputValues, List expectedValues, String expectedDescription) { checkArgumentNotNull(inputValues); checkArgumentNotNull(expectedValues); checkArgument(inputValues.size() == expectedValues.size(), "inputValues and %s must have the same size", expectedDescription); } /** * Convenience wrapper around {@link #assertPropertyViolationCounts(String, List, List, Object, Consumer, Class[])} * to check that a String property cannot be blank. *

* Note this assumes the property doesn't have other validations, otherwise it would be unable to know the expected * violation count. * * @param propertyName the property to validate * @param object the object to validate * @param mutator the mutator function, e.g. a setter method * @param groups the group or list of groups targeted for validation (defaults to Default) * @param the object type */ public void assertStringPropertyCannotBeBlank(String propertyName, T object, Consumer mutator, Class... groups) { var inputs = inputs(A_STRING_VALUE, "", " ", null); var expectedViolations = expectedViolations(0, 1, 1, 1); assertPropertyViolationCounts(propertyName, inputs, expectedViolations, object, mutator, groups); } /** * Convenience wrapper around {@link #assertPropertyViolationCounts(String, List, List, Object, Consumer, Class[])} * to check that a String property must be null. *

* Note this assumes the property doesn't have other validations, otherwise it would be unable to know the expected * violation count. * * @param propertyName the property to validate * @param object the object to validate * @param mutator the mutator function, e.g. a setter method * @param groups the group or list of groups targeted for validation (defaults to Default) * @param the object type */ public void assertStringPropertyMustBeNull(String propertyName, T object, Consumer mutator, Class... groups) { var inputs = inputs(A_STRING_VALUE, "", " ", null); var expectedViolations = expectedViolations(1, 1, 1, 0); assertPropertyViolationCounts(propertyName, inputs, expectedViolations, object, mutator, groups); } /** * Convenience wrapper around {@link #assertPropertyViolationCounts(String, List, List, Object, Consumer, Class[])} * to check that a String property has no violations. *

* Note this assumes the property doesn't have other validations, otherwise it would be unable to know the expected * violation count. * * @param propertyName the property to validate * @param object the object to validate * @param mutator the mutator function, e.g. a setter method * @param groups the group or list of groups targeted for validation (defaults to Default) * @param the object type */ public void assertStringPropertyHasNoViolations(String propertyName, T object, Consumer mutator, Class... groups) { var inputs = inputs(A_STRING_VALUE, "", " ", null); var expectedViolations = expectedViolations(0, 0, 0, 0); assertPropertyViolationCounts(propertyName, inputs, expectedViolations, object, mutator, groups); } /** * Convenience wrapper around {@link #assertPropertyViolationCounts(String, List, List, Object, Consumer, Class[])} * to check that an Instant property cannot be null. *

* Note this assumes the property doesn't have other validations, otherwise it would be unable to know the expected * violation count. * * @param propertyName the property to validate * @param object the object to validate * @param mutator the mutator function, e.g. a setter method * @param groups the group or list of groups targeted for validation (defaults to Default) * @param the object type */ public void assertInstantPropertyCannotBeNull(String propertyName, T object, Consumer mutator, Class... groups) { var inputs = inputs(Instant.now(), null); var expectedViolations = expectedViolations(0, 1); assertPropertyViolationCounts(propertyName, inputs, expectedViolations, object, mutator, groups); } /** * Convenience wrapper around {@link #assertPropertyViolationCounts(String, List, List, Object, Consumer, Class[])} * to check that an Instant property must be null. *

* Note this assumes the property doesn't have other validations, otherwise it would be unable to know the expected * violation count. * * @param propertyName the property to validate * @param object the object to validate * @param mutator the mutator function, e.g. a setter method * @param groups the group or list of groups targeted for validation (defaults to Default) * @param the object type */ public void assertInstantPropertyMustBeNull(String propertyName, T object, Consumer mutator, Class... groups) { var inputs = inputs(Instant.now(), null); var expectedViolations = expectedViolations(1, 0); assertPropertyViolationCounts(propertyName, inputs, expectedViolations, object, mutator, groups); } /** * Convenience wrapper around {@link #assertPropertyViolationCounts(String, List, List, Object, Consumer, Class[])} * to check that a ZonedDateTime property cannot be null. *

* Note this assumes the property doesn't have other validations, otherwise it would be unable to know the expected * violation count. * * @param propertyName the property to validate * @param object the object to validate * @param mutator the mutator function, e.g. a setter method * @param groups the group or list of groups targeted for validation (defaults to Default) * @param the object type */ public void assertZonedDateTimePropertyCannotBeNull(String propertyName, T object, Consumer mutator, Class... groups) { var inputs = inputs(ZonedDateTime.now(), null); var expectedViolations = expectedViolations(0, 1); assertPropertyViolationCounts(propertyName, inputs, expectedViolations, object, mutator, groups); } /** * Convenience wrapper around {@link #assertPropertyViolationCounts(String, List, List, Object, Consumer, Class[])} * to check that a ZonedDateTime property must be null. *

* Note this assumes the property doesn't have other validations, otherwise it would be unable to know the expected * violation count. * * @param propertyName the property to validate * @param object the object to validate * @param mutator the mutator function, e.g. a setter method * @param groups the group or list of groups targeted for validation (defaults to Default) * @param the object type */ public void assertZonedDateTimePropertyMustBeNull(String propertyName, T object, Consumer mutator, Class... groups) { var inputs = inputs(ZonedDateTime.now(), null); var expectedViolations = expectedViolations(1, 0); assertPropertyViolationCounts(propertyName, inputs, expectedViolations, object, mutator, groups); } /** * Convenience wrapper around {@link #assertPropertyViolationCounts(String, List, List, Object, Consumer, Class[])} * to check that a Long property cannot be null. *

* Note this assumes the property doesn't have other validations, otherwise it would be unable to know the expected * violation count. * * @param propertyName the property to validate * @param object the object to validate * @param mutator the mutator function, e.g. a setter method * @param groups the group or list of groups targeted for validation (defaults to Default) * @param the object type */ public void assertLongPropertyCannotBeNull(String propertyName, T object, Consumer mutator, Class... groups) { var inputs = inputs(42L, null); var expectedViolations = expectedViolations(0, 1); assertPropertyViolationCounts(propertyName, inputs, expectedViolations, object, mutator, groups); } /** * Convenience wrapper around {@link #assertPropertyViolationCounts(String, List, List, Object, Consumer, Class[])} * to check that a Long property must be null. *

* Note this assumes the property doesn't have other validations, otherwise it would be unable to know the expected * violation count. * * @param propertyName the property to validate * @param object the object to validate * @param mutator the mutator function, e.g. a setter method * @param groups the group or list of groups targeted for validation (defaults to Default) * @param the object type */ public void assertLongPropertyMustBeNull(String propertyName, T object, Consumer mutator, Class... groups) { var inputs = inputs(42L, null); var expectedViolations = expectedViolations(1, 0); assertPropertyViolationCounts(propertyName, inputs, expectedViolations, object, mutator, groups); } /** * Convenience wrapper around {@link #assertPropertyViolationCounts(String, List, List, Object, Consumer, Class[])} * to check that an Integer property cannot be null. *

* Note this assumes the property doesn't have other validations, otherwise it would be unable to know the expected * violation count. * * @param propertyName the property to validate * @param object the object to validate * @param mutator the mutator function, e.g. a setter method * @param groups the group or list of groups targeted for validation (defaults to Default) * @param the object type */ public void assertIntegerPropertyCannotBeNull(String propertyName, T object, Consumer mutator, Class... groups) { var inputs = inputs(84, null); var expectedViolations = expectedViolations(0, 1); assertPropertyViolationCounts(propertyName, inputs, expectedViolations, object, mutator, groups); } /** * Convenience wrapper around {@link #assertPropertyViolationCounts(String, List, List, Object, Consumer, Class[])} * to check that an Integer property must be null. *

* Note this assumes the property doesn't have other validations, otherwise it would be unable to know the expected * violation count. * * @param propertyName the property to validate * @param object the object to validate * @param mutator the mutator function, e.g. a setter method * @param groups the group or list of groups targeted for validation (defaults to Default) * @param the object type */ public void assertIntegerPropertyMustBeNull(String propertyName, T object, Consumer mutator, Class... groups) { var inputs = inputs(84, null); var expectedViolations = expectedViolations(1, 0); assertPropertyViolationCounts(propertyName, inputs, expectedViolations, object, mutator, groups); } /** * Convenience wrapper around {@link #assertPropertyViolationCounts(String, List, List, Object, Consumer, Class[])} * to check that an {@code enum} property cannot be null. Tests all the values in the specified * {@code enumClass} as well as a {@code null} value. *

* Note this assumes the property doesn't have other validations, otherwise it would be unable to know the expected * violation count. * * @param propertyName the property to validate * @param object the object to validate * @param enumClass the type of the input {@link Enum} * @param mutator the mutator function, e.g. a setter method * @param groups the group or list of groups targeted for validation (defaults to Default) * @param the object type * @param the enum type */ public > void assertEnumPropertyCannotBeNull(String propertyName, T object, Class enumClass, Consumer mutator, Class... groups) { E[] enumConstants = enumClass.getEnumConstants(); List inputs = inputs(enumConstants); inputs.add(null); List expectedViolations = new ArrayList<>(Collections.nCopies(enumConstants.length, 0)); expectedViolations.add(1); assertPropertyViolationCounts(propertyName, inputs, expectedViolations, object, mutator, groups); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy