Skip to content

Instantly share code, notes, and snippets.

@rponte
Last active February 22, 2025 02:19
Show Gist options
  • Save rponte/cb1f027f89f964a3def81978eb3f25bc to your computer and use it in GitHub Desktop.
Save rponte/cb1f027f89f964a3def81978eb3f25bc to your computer and use it in GitHub Desktop.
Spring Boot Testing: How to test the Bean Validation annotations in domain and DTO objects without starting the whole Spring Boot Context
import org.assertj.core.groups.Tuple;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import java.util.Set;
import static org.assertj.core.api.AssertionsForClassTypes.tuple;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Unfortunately there's no Bean Validation test-slice annotation
* https://docs.spring.io/spring-boot/appendix/test-auto-configuration/slices.html
*/
@SpringBootTest(
classes = ValidationAutoConfiguration.class // Configures ONLY the validation infrastructure
)
@ActiveProfiles("test")
class BookRequestTest {
@Autowired
private Validator validator;
/**
* Initializes the Bean Validation in standalone mode.
*
* ⚠️ IMPORTANT: The problem with this approach is that it might not match the Spring's configuration,
* which might produce a different result from that of production.
*/
// @BeforeEach
// public void setUp() {
// ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
// validator = factory.getValidator();
// }
@Test
@DisplayName("must be a valid book")
void t1() {
// scenario
BookRequest book = new BookRequest("9788550800653", "Domain-Driven Design", "DDD");
// action
Set<ConstraintViolation<BookRequest>> constraints = validator.validate(book);
// validation
assertEquals(0, constraints.size());
}
@Test
@DisplayName("must NOT be a valid book")
void t2() {
// scenario
BookRequest book = new BookRequest("97885-invalid", "a".repeat(121), "");
// action
Set<ConstraintViolation<BookRequest>> constraints = validator.validate(book);
// validation
assertConstraintErrors(constraints,
tuple("isbn", "invalid ISBN"),
tuple("title", "size must be between 0 and 120"),
tuple("description", "must not be blank")
);
}
/**
* Asserts that the constraint errors match the expected informed tuples
*/
private <T> void assertConstraintErrors(Set<ConstraintViolation<T>> constraints, Tuple...tuples) {
assertThat(constraints)
.hasSize(tuples.length)
.extracting(
t -> t.getPropertyPath().toString(), // field name
ConstraintViolation::getMessage. // error message
)
.containsExactlyInAnyOrder(tuples)
;
}
}
import org.hibernate.validator.constraints.ISBN;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import static org.hibernate.validator.constraints.ISBN.Type.ISBN_13;
public record BookRequest(
@NotBlank
@ISBN(type = ISBN_13)
String isbn,
@NotBlank
@Size(max = 120)
String title,
@NotBlank
@Size(max = 4000)
String description
){}
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import net.jqwik.api.*;
import net.jqwik.api.constraints.*;
import net.jqwik.spring.JqwikSpringSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import java.util.Set;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
/**
* Example of Property-based Testing with jQwik and Spring Boot
*/
@JqwikSpringSupport
@SpringBootTest(
classes = ValidationAutoConfiguration.class // Configures ONLY the validation infrastructure
)
@ActiveProfiles("test")
class BookRequestPropertyBasedTest {
@Autowired
private Validator validator;
@Property
void validBookRequest(@ForAll("validIsbn13") String isbn,
@ForAll @AlphaChars @Whitespace @Chars({'.', ',', '!', '?'}) @NotBlank @StringLength(max = 120) String title,
@ForAll("validDescription") String description) {
// scenario
BookRequest book = new BookRequest(isbn, title, description);
// action
Set<ConstraintViolation<BookRequest>> constraints = validator.validate(book);
// validation
assertThat(constraints).isEmpty();
}
@Provide
Arbitrary<String> validDescription() {
return Arbitraries.strings()
.alpha()
.withChars(' ', '.', ',', '!', '?')
.ofMinLength(1)
.ofMaxLength(4000)
.filter(text -> !text.isBlank());
}
@Provide
Arbitrary<String> validIsbn13() {
return Arbitraries.strings()
.withChars('0', '9') // Generate only numeric characters
.ofLength(12) // Generate the first 12 digits of the ISBN-13
.map(Isbn13CheckDigitAppender::appendCheckDigit); // Add the check digit
}
/**
* Generates an ISBN13 check digit and appends it to the informed ISBN
*/
class Isbn13CheckDigitAppender {
static String appendCheckDigit(String isbnWithoutCheckDigit) {
int checkDigit = calculateIsbn13CheckDigit(isbnWithoutCheckDigit);
return isbnWithoutCheckDigit + checkDigit;
}
private static int calculateIsbn13CheckDigit(String isbnWithoutCheckDigit) {
int sum = 0;
for (int i = 0; i < isbnWithoutCheckDigit.length(); i++) {
int digit = Character.getNumericValue(isbnWithoutCheckDigit.charAt(i));
sum += (i % 2 == 0) ? digit : digit * 3; // Alternate between multiplying by 1 and 3
}
int remainder = sum % 10;
return (remainder == 0) ? 0 : 10 - remainder; // Calculate the check digit
}
}
}
@rponte
Copy link
Author

rponte commented Dec 17, 2024

If you need to test the Bean Validation annotations from JPA/Hibernate entities (and repositories), follow this other approach.

@rponte
Copy link
Author

rponte commented Jan 15, 2025

Don't forget to configure the jqwik dependencies on Maven's pom.xml:

<!-- ****************** -->
<!-- jQwik for Property-based Testing -->
<!-- ****************** -->
<dependency>
	<groupId>net.jqwik</groupId>
	<artifactId>jqwik-spring</artifactId>
	<version>0.12.0</version>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>net.jqwik</groupId>
	<artifactId>jqwik</artifactId>
	<version>1.8.5</version>
	<scope>test</scope>
</dependency>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment