Spring Boot’s DataJpaTest: Do you flush and clear?

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.

3 thoughts on “Spring Boot’s DataJpaTest: Do you flush and clear?”

  1. I just want to thank you, man.

    If you allow me I will explain my scenario using your example. I have a native @Modifying @Query where I was adding articles to an existing already attached User.

    After running this native query I was using JpaRepository findById to reload this attached User and check if the articles were being saved correctly. The list was not being populated neither in LAZY nor in EAGER mode, but natively querying the DB showed me that the data was there.

    I tried to do the same you recommend in this article, but using @PersitenceContext and EntityManager: it did not work. The key here is using TestEntityManager, which I did not know before reading your article.

    Thanks. I hope Google also indexes comments to help people that are going through the same problem.

  2. I was just searching for this for a test I was trying to make work properly, thank you!

Comments are closed.