Common Issues Encountered while unit testing Spring Boot
Published on

Common Issues Encountered while unit testing Spring Boot

Authors

This article will discuss the most common issues encountered when writing unit tests for spring application. First will talk about some basic ways you can unit test your application.

  • Standard Tests – They are regular unit tests, which you can use to test public functions & classes. Everything related to the Spring context is disabled.

  • Sliced Tests – Only partial Spring context is enabled. If you're testing controllers & rest controllers, then you'd use @WebMvcTest. If you're trying JPA repositories, you will use @DataJpaTest, etc. These tests are quicker than fully loaded tests.

  • Full Application Tests – We use @SpringBootTest, which loads the full Spring context. This testing is costly, and we should avoid testing simple functions, etc. We should only use it for integration testing, i.e., verifying if everything is working perfectly.

Standard Tests

This testing is very similar to regular unit tests. If we want to test something, we'll have to create an object or mock the thing. Spring will not help in dependency injection, etc. We can add the below dependency in the pom.xml file for unit testing as a general practice.

pom.xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>

Including spring-boot-starter-test will add all the required libraries for unit testing like JUnit, Mockito, etc.

You can add @ExtendWith(MockitoExtension.class) to your Test class as a best practice. Doing so will allow you to use @Mock/@Spy in your class variables by automatically instantiating it.

@ExtendWith(MockitoExtension.class)
@SpringBootTest
class ApplicationTests {
  @Mock TestComponent testComponent;

  @Test
  public void Test() {
    assertNotNull(testComponent);
  }
}

Sliced Tests

We use this for complex interactions in the system, i.e., if we have repositories that our controller or logic is using, we can use @DataJpaTest, which will inject those repositories for testing. We can also use @Mock, but mocking every repository's functionality is cumbersome. So Spring injecting those objects would help in writing efficient & quick tests.

@DataJpaTest

We'll be using repositories that interact with the databases, and we'll need to make sure testing is replicable & accurate every time the test runs. If we had to point to MySql or SQL Server databases, it would be difficult to test, as we might need to add/modify/delete records. For unit testing, the H2 database is recommended. You can add the below dependency in your project's pom.xml file.

pom.xml
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>test</scope>
</dependency>

The scope is "test", as we'll be needing it only while executing tests. After adding this dependency, we'll need to add the below into our application.properties file. This file will be present in the test/resources folder, only applicable to tests.

test/resources/application.properties
# To create in-memory database,
spring.datasource.url=jdbc:h2:mem:testdb;

# If we want to see the SQL generated by Hibernate
spring.jpa.show-sql=true

# If we want SQL to be formatted
spring.jpa.properties.hibernate.format_sql=true

# If we want hibernate to create the tables in H2 based on the @Entity
spring.jpa.hibernate.ddl-auto=update

Below is an example of how we'll need to create our Test class

@ExtendWith(MockitoExtension.class) // This will allow us to add @Mock/@Spy
@DataJpaTest // Allows Spring to Autowire repositories
@ContextConfiguration(classes = Application.class) // Needed to Autowire respective repositories, otherwise they will not be injected i.e. will be null
public class TestClass {
@Autowired
    private JpaRepositoryA jpaRepositoryA; // If this repository is extends JpaRepository you can use @Autowired

    @Mock
    private NonJpaRepositoryA nonJpaRepositoryA; // This will not be injected by @DataJpaTest, as this class doesn't extend JpaRepository, even if @Repository is used.

    @Test
    public void Test1() {
    	Assert.assertNotNull(jpaRepositoryA);
  }
}

@WebMvcTest

@WebMvcTest is only going to scan the controller you've defined and the MVC infrastructure. That's it. So if your controller has some dependency on other beans from your service layer, the test won't start until you either load that config yourself or provide a mock for it. Doing so is much faster as we only load a tiny portion of your app. This annotation uses slicing.

@RunWith(SpringRunner.class)
@WebMvcTest(HelloController.class)
public class HelloControllerApplicationTest {
    @Autowired
    private MockMvc mvc;

    @Test
    public void getHello() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().string(equalTo("Greetings from Spring Boot!")));
    }
}

Full Application Tests

Suppose you want to test your application end to end, i.e., with no mocking or dummy variables. You can make use of @SpringBootTest, which will allow you to @Autowired for any component needed for testing. Whatever you can do while developing your Spring Boot Application, you can do the same during unit testing. But these tests can become quite expensive as each test will start a full-blown spring boot context. So, unit tests will take a lot of time to complete.

Nothing new needs to be added to the application.properties for you to run @SprintBootTest. You just need to make sure you're using the H2 database for your testing.

Common issues which you'll encounter while Unit Testing Spring Boot Application

Entity/Table has a schema; how to handle it in the H2 database during unit testing?

H2 database allows tables with schema; while unit testing, we need to make sure to create the schema's before Hibernate loads the entities & creates tables, etc. We need to add RUNSCRIPT to our datasource URL in the application.properties to create our schemas at connection time.

test/resources/application.properties
spring.datasource.url=jdbc:h2:mem:testdb;RUNSCRIPT from '~/schema.sql'
test/resources/schema.sql
create schema dbo;

Here schema.sql is a file that should be created in our test/resources folder. You can make the required schemas like below. Here I've mentioned dbo, the most common schema name, but you can have different schema names based on application.

@Repository in @DataJpaTest is initialized to null, not getting autowired?

Suppose your repository is not extending JpaRepository if you're only using @Repository for it. Then it acts like @Component, etc. They will not be automatically autowired if you use @DataJpaTest, which doesn't load the entire spring context. To auto-wire those, you'll need to use @Import like below.

@Import({ClassA.class, ClassB.class})

Otherwise, you can auto-wire those with the help of @SpringBootTest, but this will increase unit test time as it's loading the full context.

While unit testing, the repository is not updating values to the database?

This issue can happen in unit tests if we test an @Transactional annotation method. What will happen is that after the function gets executed, it will get rolled back automatically. Usually, when we run a unit test with the H2 database, a rollback occurs after the unit test completes. So, to rectify this issue, we'll need to use the below.

@Transactional(propagation = Propagation.NOT_SUPPORTED)

But there is one caveat: we must roll back the updates done to the database manually after each unit test. We can do this by using @Sql annotation. Please see the below example to understand how you can configure it.

@DataJpaTest
@Transactional(propagation = Propagation.NEVER)
class ApplicationTests {
@Autowired
UserRepository userRepository;

    @Sql("/clear-tables.sql") // Should be called for every test
    @Test
    void TestExample1() {
    	assertNotNull(userRepository);
    	UserEntity user = UserEntity.builder().name("CodingJump").build();
    	userRepository.save(user);
    	assertEquals(1L, userRepository.count());
    }

    @Sql("/clear-tables.sql") // Should be called for every test
    @Test
    void TestExample2() {
    	assertNotNull(userRepository);
    	UserEntity user = UserEntity.builder().name("CodingJump").build();
    	userRepository.save(user);
    	assertEquals(1L, userRepository.count());
    }

}
test/resources/clear-tables.sql
delete from user;

Spring Data JPA Update @Query not updating?

Let's say you have a below statement where you're updating your database with @Query, but when you test your application typically with unit test, the updates don't seem to reflect.

@Modifying
@Transactional
@Query("UPDATE user SET firstname = :firstname, lastname = :lastname WHERE id = :id")
public void update(@Param("firstname") String firstname, @Param("lastname") String lastname, @Param("id") Long id);

The issue looks similar to the previous topic, which we covered earlier.

Usually, the entity manager uses the first-level cache to store the updates. When creating an integration test on a statement saving an object, it is recommended to flush the entity manager to avoid any false-negative cases.

You can do this by adding to the above query.

@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE user SET firstname = :firstname, lastname = :lastname WHERE id = :id")
public void update(@Param("firstname") String firstname, @Param("lastname") String lastname, @Param("id") Long id);

Your unit tests will work perfectly fine as they get automatically flushed to the database, but this is not the right approach for the following reasons.

  • You should not modify production code for unit tests to work. As including clearAutomatically = true is not needed for production. This statement is just required for unit tests to work Updating the database for every update will slow your program in production. Therefore, we are invalidating the primary reason for Cache in Hibernate. Instead, hibernate works fast due to batch updates to the database, i.e., storing intermediate results in cache.

To solve the above case, you can use @Transactional(propagation = Propagation.NEVER) in your test so that transactions are disabled for unit tests. If you add this statement, you don't need to use other hacks for flushing to the database for your unit tests. But make sure to delete your updates before the next test.

Conclusion

I hope this blog explains all the caveats of unit testing with Spring Boot, and please let me know in the comments if you find any different types of issues while working on unit tests. I will update this post accordingly.