Faster Testing of the JPA Repository in Spring Applications

In the ‘Clean Code‘ book, Uncle Bob defined the five characteristics of a clean test. Those characteristics have the acronym FIRST; which stands for Fast, Independent, Repeatable, Self-Validating, and Timely.  

For this post, let’s focus on the ‘Fast‘ attribute!

Here, I will demonstrate (through code) how to make testing the repository layer of the Spring Application faster. Thus, having a faster feedback loop.

1. Project Description

For this tutorial, we will be using a Spring application that performs simple DB operations on a book repository. There will be no need to write any Controller or Service code.

1.1. Project Code Structure

The project consists of the following Java classes:

  1. Book.java: This class defines the Book entity class.
  2. BookRepository.java: This interface extends the ‘JpaRepository’ interface. In addition to the inherited functionalities, we have a new method that finds books by ‘Author Name’.
  3. SpringBookRepositoryTest.java: This is a SpringBootTest, thus, it starts a Spring instance before running the tests.
  4. NoSpringBookRepositoryTest.java: This test suite does not require running Spring when running tests.

Both test classes above contain the same 8 test cases that cover 8 methods of the BookRepository interface.

Note: In reality, we don’t need to unit test all the JpaRepository methods like I did here, since we can assume it is fully tested by the Spring Framework. Nevertheless, the same logic can be applied for tests that require initializing an instance of the repository classes.

2. So, what is the problem?

Spring simplifies writing the repository layer for us. By extending the interface ‘JpaRepository’, we can benefit from a wide range of functions (ex: findOne, findAll, exists, etc.) without writing any SQL or Java code.

This is all provided thanks to the ‘Dependency Injection’ done by the Spring Framework. However, the price we pay for that is in the tests; because to run any test, we need to start Spring. Thus, losing a lot of precious time!

2.1. Illustration

Let’s analyze the problem!

To run the class SpringBookRepositoryTest.java, we need to:

  1. Tag the test with the ‘@SpringBootTest‘ annotation. This will be used to run Spring Boot server before running the test.
  2. Tag the BookRepository variable with ‘@Autowired‘ annotation. This will be used by Spring to inject an instance of the BookRepository

Here is a code snippet of the test class (SpringBookRepositoryTest.java):

@SpringBootTest(classes = SpringJPATestingApplication.class)
class SpringBookRepositoryTest {
    @Autowired
    private BookRepository bookRepository;

    private Book cleanCode;
    private Book refactoring;
    private Book cleanArchitecture;

    @BeforeEach
    private void setup() {
        cleanCode = new Book("978-0132350884", "Clean Code", "Robert Martin");
        refactoring = new Book("978-0201485677", "Refactoring: Improving the Design of Existing Code", "Martin Fowler");
        cleanArchitecture = new Book("978-0134494166", "Clean Architecture: A Craftsman's Guide to Software Structure and Design", "Robert Martin");

        cleanCode = bookRepository.save(cleanCode);
        refactoring = bookRepository.save(refactoring);
        cleanArchitecture = bookRepository.save(cleanArchitecture);
    }

    @Test
    public void it_finds_a_book_by_an_id() {
        assertEquals(cleanCode, bookRepository.findById(cleanCode.getBookId()).get());
        assertEquals(refactoring, bookRepository.findById(refactoring.getBookId()).get());
        assertEquals(cleanArchitecture, bookRepository.findById(cleanArchitecture.getBookId()).get());
    }
...
...
}

The above tests work perfectly!

But, let us have a look at the timing!

2.2. Test Cases Timings

The below table illustrates the time taken for each test:

Test NameRun 1Run 2Run 3
itSavesTheBooksToTheDatabase 111012
itFindsAllTheBooksInTheDatabase 111010
itFindsABookByAnId 151619
itFindsBooksByAuthorName 494645
itReturnsTrueIfBookExists 280245270
itCountsTheNumberOfBooksInRepository 131214
itDeletesABook141518
itDeletesABookById162019
Test Class Initialization535947124819
SpringBootTest Initialization781847910
Total733067897046

3. The solution

There are two options to solve the above problem:

  1. Write a custom implementation of the BookRepository interface.
  2. Use the ‘JPARepositoryFactory‘ class to create an instance of the BookRepository.

Both options do not require us to start SpringBoot, but, the first is complex, faulty, and time wasting. Thus, we are left with second option!

3.1. Illustration

To use the ‘JPARepositoryFactory‘ and the dependency on SpringBoot, we need to do the following:

  1. Use the EntityManager class to load the InMemory database configuration (H2 in our case). This also requires us to include the Persistence.xml file that defines the H2 configuration.
  2. Use ‘JpaRepositoryFactory‘ class to inject an implementation of the BookRepository interface.

Below is a code snippet of test file (NoSpringBookRepositoryTest.java):

class NoSpringBookRepositoryTest {

    private static EntityManagerFactory entityManagerFactory;
    private static EntityManager entityManager;
    private static BookRepository bookRepository;

    private Book cleanCode;
    private Book refactoring;
    private Book cleanArchitecture;

    @BeforeAll
    public static void setup() {
        entityManagerFactory = Persistence.createEntityManagerFactory("InMemoryRepository");
        entityManager = entityManagerFactory.createEntityManager();
        JpaRepositoryFactory factory = new JpaRepositoryFactory(entityManager);
        bookRepository = factory.getRepository(BookRepository.class);
    }

    @BeforeEach
    public void before() {
        entityManager.getTransaction().begin();
        cleanCode = new Book("978-0132350884", "Clean Code", "Robert Martin");
        refactoring = new Book("978-0201485677", "Refactoring: Improving the Design of Existing Code", "Martin Fowler");
        cleanArchitecture = new Book("978-0134494166", "Clean Architecture: A Craftsman's Guide to Software Structure and Design", "Robert Martin");

        cleanCode = bookRepository.save(cleanCode);
        refactoring = bookRepository.save(refactoring);
        cleanArchitecture = bookRepository.save(cleanArchitecture);
    }

    @Test
    public void it_finds_a_book_by_an_id() {
        assertEquals(cleanCode, bookRepository.findById(cleanCode.getBookId()).get());
        assertEquals(refactoring, bookRepository.findById(refactoring.getBookId()).get());
        assertEquals(cleanArchitecture, bookRepository.findById(cleanArchitecture.getBookId()).get());
    }
    ...
}

Now that we have made this test totally independent from the the Spring Framework by removing all the Spring annotations, Let’s see if the timings improved!

3.2. Test Cases Timings

The below table displays the new timings:

Test NameRun-1Run-2 Run-3
itSavesTheBooksToTheDatabase222
itFindsAllTheBooksInTheDatabase5610
itFindsABookByAnId344
itFindsBooksByAuthorName566
itReturnsTrueIfBookExists121914
itCountsTheNumberOfBooksInRepository556
itDeletesABook7118
itDeletesABookById8815
Test Class Initialization275298296
SpringBootTest Initialization000
Total322359361

By comparing the timings in the 2 tables above, we can clearly notice an average gain of 6,700 ms for each test class in the second table!

4. Conclusion

Tests become a liability only if they are in a dirty state. On the contrary, writing F.A.S.T tests is critical to establishing a fast feedback loop and thus increasing confidence in the code.

Test code is as important as production code! It is paramount to have both maintain the same quality standards.

Finally, you can find the complete code under this Github repository.

5. Resources

  1. JpaRepositoryFactory – Spring Documentation
  2. JUnit Insights
  3. Clean Code Book by Robert Martin
  4. Picture take from Pixabay

Comments

Leave a comment