7 minutes read

Interacting with databases is one of the main parts of web application development. That's why it's important to make sure that your repositories work the way you expect them to. In this topic, you'll learn how to test the repositories with Spring Framework tools.

Setting up the testing

All the basic tools we need today for testing repositories are added to the spring-boot-starter-test dependency. It includes a lot of popular libraries for testing, such as JUnit, AssertJ, and Mockito. This dependency is added to a Spring Boot project by default. Of course, we need to have a driver for the SQL database since we want to work with repositories. In this topic we'll work with the H2 database, so don't forget to add the com.h2database dependency to your project.

Today we are going to test the repository made for an electronics store app. Our project has the following entity:

Java
@Entity
public class Product {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String type;
    private String model;
    private int number;
    private int price;

    //constructors, getters, and setters
}
Kotlin
@Entity
data class Product(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null, // Initialized to null for auto-generation
    var type: String,
    var model: String,
    var number: Int,
    var price: Int
)

It clearly makes no sense to test the built-in repository methods since we don't need to test the Spring Framework functionality. However, it is useful to test the custom complicated methods because we want to be sure everything works as we planned. So, we will test the following methods from the repository:

Java
public interface ProductRepository extends CrudRepository<Product, Long> {
    List<Product> findAllByType(String type);
    Product findTopByOrderByPriceDesc();
    Product findByNumberAndPrice(int number, int price);
}
Kotlin
interface ProductRepository : CrudRepository<Product, Long> {
    fun findAllByType(type: String): List<Product>
    fun findTopByOrderByPriceDesc(): Product?
    fun findByNumberAndPrice(number: Int, price: Int): Product?
}

Also, let's create the scripts for the database pre-population, which will be used later. We will put them in the test/resources folder because we don't need to launch these scripts in our main application. Here is schema.sql for defining the table structure:

DROP TABLE IF EXISTS product;
CREATE TABLE product (
    id IDENTITY PRIMARY KEY,
    type VARCHAR(50),
    model VARCHAR(50),
    number INT,
    price INT
);

And the data.sql script to insert some data into the table:

INSERT INTO product(type, model, number, price) VALUES('phone', 'iPhone', 100, 1000);
INSERT INTO product(type, model, number, price) VALUES('TV', 'Samsung', 25, 1400);
INSERT INTO product(type, model, number, price) VALUES('vacuum cleaner', 'LG', 60, 100);
INSERT INTO product(type, model, number, price) VALUES('washing machine', 'Toshiba', 10, 500);
INSERT INTO product(type, model, number, price) VALUES('phone', 'Huawei', 140, 800);
INSERT INTO product(type, model, number, price) VALUES('TV', 'LG', 15, 900);

Testing repositories with @SpringBootTest

First, we will test our repository using the @SpringBootTest annotation. This annotation works by creating the ApplicationContext with all of the components of the project just like a usual Spring Boot application. Also, we will use @Sql annotation which will help pre-populate our database. You can find the correct way of the testing class implementation below:

Java
@SpringBootTest
@Sql({"/schema.sql", "/data.sql"})
class TestingRepositoriesApplicationTests {

    @Autowired
    private ProductRepository productRepository;

    @Test
    void testFindByTypeMethod() {
        List<Product> productList = productRepository.findAllByType("phone");
        System.out.println(productList);
        assertEquals(2, productList.size());
    }

    @Test
    void testFindMostExpensiveProduct() {
        Product product = productRepository.findTopByOrderByPriceDesc();
        assertEquals(1400, product.getPrice());
    }

    @Test
    void testFindByPriceAndNumber() {
        Product product = productRepository.findByNumberAndPrice(100, 1000);
        assertEquals("iPhone", product.getModel());
    }
}
Kotlin
@SpringBootTest
@Sql(scripts = ["/schema.sql", "/data.sql"])
class TestingRepositoriesApplicationTests {

    @Autowired
    private lateinit var productRepository: ProductRepository

    @Test
    fun testFindByTypeMethod() {
        val productList = productRepository.findAllByType("phone")
        println(productList)
        assertEquals(2, productList.size)
    }

    @Test
    fun testFindMostExpensiveProduct() {
        val product = productRepository.findTopByOrderByPriceDesc()
        assertEquals(1400, product?.price)
    }

    @Test
    fun testFindByPriceAndNumber() {
        val product = productRepository.findByNumberAndPrice(100, 1000)
        assertEquals("iPhone", product?.model)
    }
}

Let's break down this code and consider the details. The @Sql annotation makes these scripts launch before every test method executes. As you may notice, we haven't mentioned the application.properties settings. That's because the @SpringBootTest annotation creates the configuration for the in-memory H2 database with the default configuration for us. If we define the database in application.properties, our tests will use it. Moreover, we can define special settings for the tests by creating the application.properties file in the test/resources folder. Then, with both files in src/resources and tests/resources, the settings from the latter one will be chosen.

Rollbacking database changes

Let's take an empty database and try to add some data to it. In the second method, we are checking if the table still contains the data about the product we saved in the previous method:

Java
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class TransactionalApplicationTests {

    @Autowired
    private ProductRepository productRepository;

    @Order(1)    
    @Test
    void addingDataTest() {
        productRepository.save(new Product("microwave oven", "LG", 20, 200));
        Iterable<Product> productIterable = productRepository.findAll();
        assertEquals(1, IterableUtil.toCollection(productIterable).size());
    }

    @Order(2)    
    @Test
    void testIfDataFromPreviousTestMethodSaved() {
        Iterable<Product> productIterable = productRepository.findAll();
        assertEquals(1, IterableUtil.toCollection(productIterable).size());
    }
}
Kotlin
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class TransactionalApplicationTests {

    @Autowired
    private lateinit var productRepository: ProductRepository

    @Order(1)
    @Test
    fun addingDataTest() {
        productRepository.save(Product(type = "microwave oven", model = "LG", number = 20, price = 200))
        val productIterable = productRepository.findAll()
        assertEquals(1, productIterable.toList().size)
    }

    @Order(2)
    @Test
    fun testIfDataFromPreviousTestMethodSaved() {
        val productIterable = productRepository.findAll()
        assertEquals(1, productIterable.toList().size)
    }
}

We use the annotations @TestMethodOrder and @Order to define the test execution order, which is important in this case. By default, JUnit runs tests using a deterministic but unpredictable order.

We launch this test and get the following result:

intellij idea spring boot test failure

As you can see, the second test fails. According to the logs, there are no rows in the table. It happens because JPA tests are transactional, so all the database changes inside the test methods are rollbacked after the method execution. We can change this behavior with the help of the @Rollback annotation. We can put it either at the class or the method level:

Java
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Rollback(false)
class TransactionalApplicationTests {

    @Autowired
    private ProductRepository productRepository;

    @Order(1)
    @Test
    void addingDataTest() {
        productRepository.save(new Product("microwave oven", "LG", 20, 200));
        Iterable<Product> productIterable = productRepository.findAll();
        assertEquals(1, IterableUtil.toCollection(productIterable).size());
    }
    
    @Order(2)
    @Test
    void testIfDataFromPreviousTestMethodSaved() {
        Iterable<Product> productIterable = productRepository.findAll();
        assertEquals(1, IterableUtil.toCollection(productIterable).size());
    }
}
Kotlin
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
@Rollback(false)
class TransactionalApplicationTests {

    @Autowired
    private lateinit var productRepository: ProductRepository

    @Order(1)
    @Test
    fun addingDataTest() {
        productRepository.save(Product(type = "microwave oven", model = "LG", number = 20, price = 200))
        val productIterable = productRepository.findAll()
        assertEquals(1, productIterable.toList().size)
    }
    
    @Order(2)
    @Test
    fun testIfDataFromPreviousTestMethodSaved() {
        val productIterable = productRepository.findAll()
        assertEquals(1, productIterable.toList().size)
    }
}

Let's try to launch the code above and see what will happen:

intellij idea spring boot test success

Guess what? The tests are passed as expected!

Using @DataJpaTest annotation

We can use the @DataJpaTest annotation instead of the @SpringBootTest one, and we won't see any differences in all the code above from the point of view of the functionality. For example, we can use it to test the repository methods:

Java
@DataJpaTest
@Sql({"/schema.sql", "/data.sql"})
class TestingRepositoriesApplicationTests {

    @Autowired
    private ProductRepository productRepository;

    @Test
    void testFindByTypeMethod() {
        List<Product> productList = productRepository.findAllByType("phone");
        System.out.println(productList);
        assertEquals(2, productList.size());
    }

    @Test
    void testFindMostExpensiveProduct() {
        Product product = productRepository.findTopByOrderByPriceDesc();
        assertEquals(1400, product.getPrice());
    }

    @Test
    void testFindByPriceAndNumber() {
        Product product = productRepository.findByNumberAndPrice(100, 1000);
        assertEquals("iPhone", product.getModel());
    }
}
Kotlin
@DataJpaTest
@Sql(scripts = ["/schema.sql", "/data.sql"])
class TestingRepositoriesApplicationTests {

    @Autowired
    private lateinit var productRepository: ProductRepository

    @Test
    fun testFindByTypeMethod() {
        val productList = productRepository.findAllByType("phone")
        println(productList)
        assertEquals(2, productList.size)
    }

    @Test
    fun testFindMostExpensiveProduct() {
        val product = productRepository.findTopByOrderByPriceDesc()
        assertEquals(1400, product?.price)
    }

    @Test
    fun testFindByPriceAndNumber() {
        val product = productRepository.findByNumberAndPrice(100, 1000)
        assertEquals("iPhone", product?.model)
    }
}

The difference is that @DataJpaTest is made especially for JPA components testing. This annotation works by creating the ApplicationContext with JPA-related components. As we mentioned before, when we use @SpringBootTest, the ApplicationContext contains all the application components. Loading the full ApplicationContext takes more time and resources, so tests with the @DataJpaTest annotation work faster. So, when creating unit tests for repositories it's better to use the @DataJpaTest annotation. As for @SpringBootTest, it is the best choice for integration tests when you need to test the interaction between different application layers.

There is another minor difference between these annotations. Tests with @DataJpaTest annotation don't use the database defined in application.properties. They always use the auto-configured embedded database. You can change this behavior with the help of the following annotation at the class level:

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)

Conclusion

In this topic, you learned how to test your repositories and found out the pros and cons of the @DataJpaTest and @SpringBootTest annotations. Also, you learned that test methods are transactional, so all the things we do with our tables in the tests are rollbacked by default. Now you can have full control over the methods of your repositories!

17 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo