Last active
February 22, 2025 02:19
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
){} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} |
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
If you need to test the Bean Validation annotations from JPA/Hibernate entities (and repositories), follow this other approach.