Last active
January 17, 2024 19:57
-
-
Save rponte/385838088f64ab8004ba7d15de80ca34 to your computer and use it in GitHub Desktop.
Spring Boot: example of base test class for testing Repositories
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
package base; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; | |
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; | |
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; | |
import org.springframework.test.context.ActiveProfiles; | |
import org.springframework.transaction.annotation.Transactional; | |
import org.springframework.transaction.support.TransactionTemplate; | |
import java.util.function.Consumer; | |
import java.util.function.Function; | |
import static org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; | |
import static org.springframework.transaction.annotation.Propagation.NOT_SUPPORTED; | |
/** | |
* To be honest, I don't like to use the Spring Boot Test Slices (like {@code @DataJpaTest}, {@code @WebMvcTest} etc.). | |
* I mean, I always try to favor using {@code @SpringBootTest} because it's closer to the real application, and | |
* it's faster when running the whole test suite. | |
*/ | |
@DataJpaTest // Starts persistence context only. | |
@Transactional(propagation = NOT_SUPPORTED) // Disables the default transactional context on each @Test method | |
@AutoConfigureTestDatabase(replace = Replace.NONE) // Uses the actual database instead of an in-memory database like H2 | |
@ActiveProfiles("test") // Activates the testing profile (environment) | |
public abstract class SpringDataJpaIntegrationTest { | |
/** | |
* (!!!) It does NOT work properly when the transactional context is disabled. | |
* You should use the repositories instead or combine it with TransactionTemplate for example. | |
*/ | |
@Autowired | |
private TestEntityManager testEntityManager; | |
@Autowired | |
protected TransactionTemplate transactionTemplate; | |
/** | |
* Executes the function inside a transactional context and return its result | |
*/ | |
public <T> T doInTransaction(JpaTransactionFunction<T> function) { | |
function.beforeTransactionCompletion(); | |
try { | |
return transactionTemplate.execute(status -> { | |
T result = function.apply(testEntityManager); | |
return result; | |
}); | |
} finally { | |
function.afterTransactionCompletion(); | |
} | |
} | |
/** | |
* Executes the function inside a transactional context but does not return anything | |
*/ | |
public void doInTransaction(JpaTransactionVoidFunction function) { | |
function.beforeTransactionCompletion(); | |
try { | |
transactionTemplate.executeWithoutResult(status -> { | |
function.accept(testEntityManager); | |
}); | |
} finally { | |
function.afterTransactionCompletion(); | |
} | |
} | |
@FunctionalInterface | |
protected interface JpaTransactionFunction<T> extends Function<TestEntityManager, T> { | |
default void beforeTransactionCompletion() {} | |
default void afterTransactionCompletion() {} | |
} | |
@FunctionalInterface | |
protected interface JpaTransactionVoidFunction extends Consumer<TestEntityManager> { | |
default void beforeTransactionCompletion() {} | |
default void afterTransactionCompletion() {} | |
} | |
} |
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
package br.com.zup.edu.ifoodwebapp.samples.books; | |
import base.SpringDataJpaIntegrationTest; | |
import org.junit.jupiter.api.BeforeEach; | |
import org.junit.jupiter.api.DisplayName; | |
import org.junit.jupiter.api.Test; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.dao.DataIntegrityViolationException; | |
import org.springframework.transaction.TransactionSystemException; | |
import javax.validation.ConstraintViolation; | |
import javax.validation.ConstraintViolationException; | |
import java.util.Optional; | |
import static org.assertj.core.api.Assertions.*; | |
import static org.assertj.core.api.InstanceOfAssertFactories.iterable; | |
import static org.assertj.core.groups.Tuple.tuple; | |
import static org.junit.jupiter.api.Assertions.assertEquals; | |
import static org.junit.jupiter.api.Assertions.assertThrows; | |
class BookRepositoryTest extends SpringDataJpaIntegrationTest { | |
@Autowired | |
private BookRepository repository; | |
@BeforeEach | |
void setUp() { | |
repository.deleteAll(); | |
} | |
/** | |
* (!!!) Example of how to use {@code TestEntityManager} with {@code doInTransaction()} methods | |
*/ | |
@Test | |
@DisplayName("should find a book by ID") | |
void t0() { | |
// scenario | |
Book book = new Book("9788550800653", "Domain-Driven Design", "DDD - The blue book"); | |
Long id = doInTransaction(em -> { | |
return em.persistAndGetId(book, Long.class); | |
}); | |
// action | |
Optional<Book> found = repository.findById(id); | |
// validation | |
assertThat(found) | |
.isPresent().get() | |
.usingRecursiveComparison() | |
.isEqualTo(book); | |
} | |
@Test | |
@DisplayName("must save a book") | |
void t1() { | |
// scenario | |
Book book = new Book("9788550800653", "Domain-Driven Design", "DDD - The blue book"); | |
// action | |
repository.save(book); | |
// validation | |
assertThat(repository.findAll()) | |
.hasSize(1) | |
.usingRecursiveFieldByFieldElementComparator() | |
.containsExactly(book) | |
; | |
} | |
@Test | |
@DisplayName("should not save a book with invalid parameters") | |
void t2() { | |
// scenario | |
Book book = new Book("97885-invalid", "a".repeat(121), ""); | |
// action and validation | |
assertThatThrownBy(() -> { | |
repository.save(book); | |
}) | |
.isInstanceOf(TransactionSystemException.class) | |
.hasRootCauseInstanceOf(ConstraintViolationException.class) | |
.getRootCause() | |
.extracting("constraintViolations", as(iterable(ConstraintViolation.class))) | |
.extracting( | |
t -> t.getPropertyPath().toString(), | |
ConstraintViolation::getMessage | |
) | |
.containsExactlyInAnyOrder( | |
tuple("isbn", "invalid ISBN"), | |
tuple("title", "size must be between 0 and 120"), | |
tuple("description", "must not be blank") | |
) | |
; | |
// Tip: Try always to verify the side effects | |
assertEquals(0, repository.count()); | |
} | |
@Test | |
@DisplayName("should not save a book when a book with same isbn already exists") | |
void t3() { | |
// scenario | |
String isbn = "9788550800653"; | |
Book ddd = new Book(isbn, "Domain-Driven Design", "DDD - The blue book"); | |
// action | |
repository.save(ddd); | |
// validation | |
assertThrows(DataIntegrityViolationException.class, () -> { | |
Book cleanCode = new Book(isbn, "Clean Code", "Learn how to write clean code with Uncle Bob"); | |
repository.save(cleanCode); | |
}); | |
// Tip: Try always to verify the side effects | |
assertEquals(1, repository.count()); | |
} | |
@Test | |
@DisplayName("should find a book by isbn") | |
void t4() { | |
// scenario | |
String isbn = "9788550800653"; | |
Book book = new Book(isbn, "Domain-Driven Design", "DDD - The blue book"); | |
repository.save(book); | |
// action | |
Optional<Book> optionalBook = repository.findByIsbn(isbn); | |
// validation | |
assertThat(optionalBook) | |
.isPresent().get() | |
.usingRecursiveComparison() | |
.isEqualTo(book) | |
; | |
} | |
@Test | |
@DisplayName("should not find a book by isbn") | |
void t5() { | |
// scenario | |
Book book = new Book("9788550800653", "Domain-Driven Design", "DDD - The blue book"); | |
repository.save(book); | |
// action | |
String notExistingIsbn = "1234567890123"; | |
Optional<Book> optionalBook = repository.findByIsbn(notExistingIsbn); | |
// validation | |
assertThat(optionalBook).isEmpty(); | |
} | |
} |
That's how we can test our Service layer with @DatJpaTest
:
@DataJpaTest(
includeFilters = [
ComponentScan.Filter(CreateBookService.class, type = ASSIGNABLE_TYPE)
]
)
class BookRepositoryTest {
// ...
}
We can also use the @Import
annotation instead:
@Import(CreateBookService.class)
class BookRepositoryTest extends SpringDataJpaIntegrationTest {
// ...
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@DataJpaTest
@SpringBootTest