Should your tests manage transactions?


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


During my Spring Data JPA workshops and talks, I often get asked if a test case should handle transactions, or not. And if it handles the transaction if it should commit it or roll it back.

And as so often, the short answer is: it depends.

The slightly longer answer is: it depends on the tested code. Has it its own transaction handling and how is it executed in production?

Let me explain.

Test like in production

No matter what you’re testing, the goal is always the same. You want to ensure that the tested code will not cause problems in production.

And what’s the best way to achieve that?

Test the code under the same conditions as it runs in production.

So, if the tested code runs within a transaction in production, it should also run within a transaction during your tests and commit it if no error occurred. Otherwise, your test might cause problems that don’t exist in production or miss bugs that will show up immediately after deploying your application.

A missing transaction, for example can cause LazyInitializationExceptions if the tested code fetches entity objects and tries to access lazily fetched associations.

These issues are easy to find and fix. Tests that perform write operations can cause bigger issues.

If your application uses a standard flush mode, your JPA implementation tries to delay your write operations as long as possible. Due to that, some of them might not get executed until you commit your transaction. So, rolling back a transaction, like many test cases do, might prevent your JPA implementation from executing all SQL statements and hide bugs.

    As you can see, it’s important to execute the tested code under the same conditions as it runs in production.

    Who should manage the transaction

    And that gets us back to the initial question: Should a test case handle the transaction?

    That depends on the code you’re testing. Does it already manage a transaction or not?

    The tested code manages the transaction

    The situation is simple, if your test case tests a part of your business code that already manages your transaction, e.g. a service bean or maybe even an entire API call. In that case, your test should not introduce its own transaction handling!

    The main goal of a test is to ensure that the tested code will work in production as expected. And the only way to achieve that is to test it under the same conditions as it runs in production. So, if the tested code already manages a transaction, you should use this transaction handling in your tests as well.

    The tested code doesn’t manage the transaction

    If you’re testing a repository or a small part of your business code that doesn’t provide its own transaction handling, it gets a little more complex.

    You now need to know under which conditions the tested code gets executed. Has the calling code always started a transaction? Will the tested code never be part of a transaction? Or can it be called with and without an active transaction?

    Depending on your answer, you have to implement one or more test cases that test your code with or without a transactional context.

    Testing your code without a transaction is simple. You only have to call the method you want to test and validate the result.

    So, let’s talk about the situation when your test has to manage the transaction.

    Managing the transaction

    This is the situation in which I see the most mistakes during my consulting projects.

    Thanks to Spring’s @Transactional annotation, adding transaction handling to your test case seems simple. You only have to add the annotation to your test class or method.

    @SpringBootTest
    @Transactional
    public class TestQueries {
    
        private static final Logger log = LoggerFactory.getLogger(TestQueries.class);
    
        @Autowired
        private ChessPlayerRepository playerRepo;
    
        @Test
        public void findByName() {
            log.info("... findByName ...");
    
            List<ChessPlayer> players = playerRepo.findByName("%arl%", "Magnus");
            assertThat(players.size()).isEqualTo(1);
            
            ChessPlayer player = players.get(0);
            log.info(player.getFirstName() + " " + player.getLastName()); 
        }
    }

    But Spring’s transaction handling for test cases introduces an unfortunate pitfall when using Spring Data JPA. By default, Spring rolls back the transaction after completing the test case. As explained earlier, that might prevent your JPA implementation from executing some of the delayed INSERT, UPDATE and DELETE statements.

    You should, therefore, also annotate your test class or method with @Commit. This overrides Spring’s default handling and gets you the same transaction handling as you will have in production

    @SpringBootTest
    @Transactional
    @Commit
    public class TestQueries {
    
        private static final Logger log = LoggerFactory.getLogger(TestQueries.class);
    
        @Autowired
        private ChessPlayerRepository playerRepo;
    
        @Test
        public void findByName() {
            log.info("... findByName ...");
    
            List<ChessPlayer> players = playerRepo.findByName("%arl%", "Magnus");
            assertThat(players.size()).isEqualTo(1);
            
            ChessPlayer player = players.get(0);
            log.info(player.getFirstName() + " " + player.getLastName()); 
        }
    }

    Conclusion

    You should always test your code under the same conditions as it runs in production!

    That means, if the tested code manages the transaction, your test should validate it and not introduce its own transaction handling.

    If the tested code doesn’t manage its own transaction, you have to check if it will be executed as part of an active transaction or without a transaction and rebuild the same conditions in your test case. When doing that, it’s important to tell Spring to commit the transaction if the test case doesn’t fail. Otherwise, the automatic rollback might prevent the execution of some of the delayed INSERT, UPDATE, and DELETE statements.