org.kiwiproject.test.validation.ParameterizedValidationTestHelper Maven / Gradle / Ivy
Show all versions of kiwi-test Show documentation
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);
}
}