My team and me are working on a API rest using Spring boot. It's composed by controllers, services and JpaRepo interfaces. Shamefully, is a monolitic app. "There's no time for it" is the answer then I suggest to change to microservices, so we have to work with what we have.
The API is growing thunderous, that includes the unit tests. Right know, we have 1267 individual test cases and more than 300 test classes (service and controllers included).
The service test classes are like this:
@SpringBootTest(classes = Project.class)
@TestPropertySource(locations = "classpath:test.properties")
@DirtiesContext(classMode=ClassMode.AFTER_CLASS)
public class Services_CategoryTBTest {
@TestConfiguration
static class Service_CategoryTbTestContextConfiguration{
@Bean
public GenericService<CategoryModifDTO, CategoryIdDTO, String> categoryService(){
return new Service_CategoryTB_impl();
}
}
@Autowired
Service_CategoryTB_impl service;
@MockBean
CategoryRepository repository;
private void setMock(String id) {
Optional<CategoryTb> categoryTB = Optional.of(new CategoryTb());
categoryTB.get().setCodcat(id);
Mockito.when(repository.existsById(id)).thenReturn(true);
Mockito.when(repository.findById(id)).thenReturn(categoryTB);
}
private void setSeveralMock() {
CategoryTb categoryTB = new CategoryTb();
categoryTB.setCodcat("1");
Mockito.when(repository.findAll()).thenReturn(Arrays.asList(categoryTB));
}
@Test
void getSpecificSuccessfully() {
//Given
String id = "1";
setMock(id);
//When
CategoryIdDTO category = service.findById(id);
//Then
assertEquals(category.getCodcat(), id);
}
@Test
void getAllSuccessfully() {
//Given
setSeveralsMock();
//When
List<CategoryIdDTO> categorys = service.findAll();
//Then
assertEquals(categorys.size(), 1);
}
@Test
void createSuccessfully() {
//Given
CategoryIdDTO category = new CategoryIdDTO();
category.setCodcat("1");
category.setDescat("Desc");
//When
service.create(category);
//Then
Mockito.verify(repository).save(Mockito.any(CategoryTb.class));
}
@Test
void updateSuccessfully() {
//Given
String id = "1";
setMock(id);
CategoryModifDTO category = new CategoryModifDTO();
category.setDescat("Desc");
//When
service.update(id, category);
//Then
Mockito.verify(repository).save(Mockito.any(CategoryTb.class));
}
@Test
void deleteSuccessfully() {
//Given
String id = "1";
setMock(id);
//When
service.delete(id);
//Then
Mockito.verify(repository).delete(Mockito.any(CategoryTb.class));
}
@Test
void testFailIfExistsByIdCheckSuccessfully() {
//Given
Mockito.when(repository.existsById(null)).thenReturn(true);
assertThrows(HttpClientErrorException.class, () -> service.failIfExistsById(null));
}
@Test
void testFailIfNotExistsByIdCheckSuccessfully() {
//Given
Mockito.when(repository.existsById(null)).thenReturn(false);
assertThrows(ResourcesNotFoundException.class, () -> service.failIfNotExistsById(null));
}
}
And controller test classes are like this:
@WebMvcTest(controllers = {
CategoryController.class }, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class), excludeAutoConfiguration = {
SecurityAutoConfiguration.class })
@TestPropertySource(locations = "classpath:test.properties")
@DirtiesContext(classMode=ClassMode.AFTER_CLASS)
public class CategoryControllerTest {
@Autowired
private MockMvc mvc;
@MockBean
private Service_CategoryTB_impl service;
private final String baseUri = "/api/categories";
private final id = "1"
@Test
void getSpecificSuccessfully() throws Exception {
// Given
setMock(id);
// When
mvc.perform(get(baseUri + "/{id}", id)
.contentType(MediaType.APPLICATION_JSON))
.andExpectAll(
status().isOk(),
content().contentType(MediaType.APPLICATION_JSON),
jsonPath("$.codcat").value(id));
}
@Test
void getAllSuccessfully() throws Exception {
// Given
setSeveralMock();
// When and then
mvc.perform(get(baseUri)
.contentType(MediaType.APPLICATION_JSON))
.andExpectAll(
status().isOk(),
content().contentType(MediaType.APPLICATION_JSON),
jsonPath("$").isArray());
}
@Test
void createSuccessfully() throws Exception {
// Given
JSONObject cfDTO = createAsJson();
// When and then
mvc.perform(post(baseUri)
.contentType(MediaType.APPLICATION_JSON)
.content(cfDTO.toString())).andExpectAll(
status().isCreated(),
header().string("Location", baseUri + "/" id),
jsonPath("$.message").value("created."));
}
@Test
void updateSuccessfully() throws Exception {
// Given
JSONObject cfDTO = createAsJson();
// When and then
mvc.perform(put(baseUri + "/{id}", cfDTO.getString("codcat"))
.contentType(MediaType.APPLICATION_JSON)
.content(cfDTO.toString())).andExpectAll(
status().isOk(),
header().string("Location", baseUri + "/" id),
jsonPath("$.message").value("modified."));
}
@Test
void deleteSuccessfully() throws Exception {
mvc.perform(delete(baseUri + "/{id}", id))
.andExpectAll(
status().isOk(),
jsonPath("$.message").value("deleted."));
}
private JSONObject createAsJson() throws JSONException {
JSONObject categoryJson = new JSONObject();
categoryJson.put("codcat", id);
categoryJson.put("descat", "desc");
return categoryJson;
}
private void setSeveralMock() {
CategoryIdDTO categoryTB = new CategoryIdDTO();
categoryTB.setCodcat(id);
when(service.findAll()).thenReturn(Arrays.asList(categoryTB));
}
private void setMock(String id) {
CategoryIdDTO category = new CategoryIdDTO();
category.setCodcat(id);
when(service.findById(id)).thenReturn(category);
}
}
And test.properties
spring.profiles.active=test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa
logging.pattern.console=
spring.autoconfigure.exclude= \
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, \
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, \
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
The rest of the tests have practically the same structure. There's only one exception where a base64 image is managed during a test, but its size is minimun exactly for not making these tests too heavy.
I've followed this, this, an this questions. Even I found that there was a bug that seems to have been my very same problem. But following Spring Boot doc, it redirects to the latest version of junit, so I suppose that if I'm using the latest Spring Boot version, I should have the latest version of junit (or >5.3 at least). So the bug should be already solved.
Here is my POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.7</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.project</groupId>
<artifactId>project</artifactId>
<version>1.0</version>
<packaging>war</packaging>
<name>project</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.modelmapper/modelmapper -->
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
<version>1.9.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.13</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20220320</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<properties>
<configurationParameters>
junit.jupiter.conditions.deactivate = *
junit.jupiter.extensions.autodetection.enabled = true
junit.jupiter.testinstance.lifecycle.default = per_class
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = concurrent
junit.jupiter.execution.parallel.config.strategy = dynamic
junit.jupiter.execution.parallel.config.dynamic.factor = 2
</configurationParameters>
</properties>
</configuration>
</plugin>
</plugins>
</build>
</project>
I've tried every solution that I've found. Even solutions proposed by the comments of the questions mentioned. If someone can help me. Thanks in advance.
Note 1: The code is an aproximation of the original code, but it represents it close enough.
Note 2: If you can help me to speed up testing, it would be a bonus. The guy in the bug report claims that it executes ~2600 tests in 3 minutes and my tests take +20 minutes to execute. But in one of the questions, an answer suggets it's normal that a test suite takes several hours to complete.