Skip to content

Instantly share code, notes, and snippets.

@rponte
Last active January 17, 2024 19:57
Show Gist options
  • Save rponte/385838088f64ab8004ba7d15de80ca34 to your computer and use it in GitHub Desktop.
Save rponte/385838088f64ab8004ba7d15de80ca34 to your computer and use it in GitHub Desktop.
Spring Boot: example of base test class for testing Repositories
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() {}
}
}
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();
}
}
@rponte
Copy link
Author

rponte commented Oct 6, 2023

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