- Published on
Common Issues Encountered while unit testing Spring Boot
- Authors
- Name
- Rahul Neelakantan
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.
<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.
<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.
# 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.
spring.datasource.url=jdbc:h2:mem:testdb;RUNSCRIPT from '~/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.
Even if you ask Hibernate to create tables automatically by enabling this option spring.jpa.hibernate.ddl-auto=update
Hibernate will fail to create tables within the schema, so you'll need to make sure you develop the schema beforehand using RUNSCRIPT like above.
@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());
}
}
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.