Intro
First of all - DataJpaTest is a great helper from the Spring Boot framework. If you are not familiar with this class then please go ahead to read a tutorial on it and then come back! In this article I want to show how to get more of its potential.
But in short - DataJpaTest automatically configures Hibernate with in-memory database (and all Spring beans that go with it). It will also run your tests in transactions so you don't need to remove created entities manually.
The issue with plain DataJpaTest
So - we have DataJpaTest and can focus on testing the behavior of our services - sounds great. But what are the implications of this setup? Let's have a look at this test:
(Please be aware that everything before assertions is business logic which would be hidden in some service - for the sake of readability I've put this code directly into the test itself.)
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "name")
private String name;
@OneToMany(mappedBy = "author")
private List<Article> articles = new ArrayList<>();
// ...plus constructors, getters and setters
}
@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private User author;
@Column(name = "content")
private String content;
// ...plus constructors, getters and setters
}
@DataJpaTest
class BlogTests {
@Autowired
private UserRepository userRepository;
@Test
public void givenUserAndArticle_Save_ShouldPersistBothToDatabase() {
// some business logic;
// it would be inside a service usually
Article article = new Article("some content");
User user = new User("foo");
user.addArticle(article);
user = userRepository.save(user);
// the end of business logic
assertThat(user.getArticles()).hasSize(1);
}
}
Looks good, right? Unfortunately after testing the code in some real environment we find out the article is actually never saved. So let's tune our logging to see what's going on:
@DataJpaTest(properties = {
"logging.level.ROOT= WARN",
"logging.level.org.springframework.test.context.transaction= INFO",
"logging.level.org.hibernate.SQL= DEBUG"
})
And here is the output:
2020-01-19 17:45:16.563 INFO 1844 --- [ main] o.s.t.c.transaction.TransactionContext : Began transaction (1) for test context [DefaultTestContext@21b2e768 testClass = BlogTests, ...]
2020-01-19 17:45:16.630 DEBUG 1844 --- [ main] org.hibernate.SQL : call next value for hibernate_sequence
Hibernate: call next value for hibernate_sequence
2020-01-19 17:45:16.733 INFO 1844 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test: [DefaultTestContext@21b2e768 testClass = BlogTests, ...]
We can see the transaction started and rollbacked but our insert query is missing. It's because the save
method does not guarantee the insert will happen immediately when run inside a transaction. In our case the insert is delayed and right after save
there is a rollback from DataJpaTest - so there's no need to do DB inserts.
Furthermore we are asserting properties on the entity instance coming from the block with business logic - but we can (and IMHO should) try to fetch it again and see if eveything was saved.
The solution
So let's make the test fail:
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager testEntityManager;
@Test
public void givenUserAndArticle_Save_ShouldPersistBothToDatabase() {
// some business logic;
// it would be inside a service usually
Article article = new Article("some content");
User user = new User("foo");
user.addArticle(article);
user = userRepository.save(user);
// end of business logic
// all good here
assertThat(user.getArticles()).hasSize(1);
// forces synchronization to DB
testEntityManager.flush();
// clears persistence context
// all entities are now detached and can be fetched again
testEntityManager.clear();
Optional<User> fetchedUser = userRepository.findById(user.getId());
assertThat(fetchedUser).isPresent();
// the article must be saved manually
// OR User#articles should have a proper CascadeType
assertThat(fetchedUser.get().getArticles())
.withFailMessage("the article was never persisted")
.hasSize(1);
}
We use flush to force synchronization (all delayed SQL queries will be done) and the clear method to allow fetching entities again. After that we fetch User entity from database and check that all entities are the way they should be.
And that's it. With this approach you can catch misconfigurations of JPA entities (wrong CascadeType, missing MappedSuperclass), bugged business logic (accidentally saving two entities with the same unique key; data not passing validation constraints) and more. The test is not perfect - because quite frankly all tests are a compromise of speed, simplicity, scope and type of bugs you will catch - but with little effort you can make it a bit more robust.
Thanks for reading this far and share your thougts in the comments!
(Code from this article can be downloaded from my Github - feel free to have a look and play with it.)
Did you like the article? Feel free to share it with your colleagues. To get nofications about more blog posts: follow me on Twitter or subscribe to RSS of blog posts.