Home > Mobile >  java.lang.OutOfMemoryError during unit tests
java.lang.OutOfMemoryError during unit tests

Time:01-12

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.

CodePudding user response:

One of the problems I see is that that you are abusing @SpringBootTest for things that should be a basic unit test. You should only use @SpringBootTest for full integration tests not for unit tests.

The problem with this is that for each permuation of @TestPropertySource, @MockBean and others (see documentation). It will load a full application and it will be cached (see the aforementioned documentation). This is not only slow but will also lead to memory issues with large applications.

Instead you should write a basic unit test using Mockito (which you already use through @MockBean).

So rewrite those tests that follow said pattern to use plain Mockito instead of @SpringBootTest. This will greatly speed up your test runs and reduce the amount of memory needed.

@ExtendWith(MockitoExtension.class)
public class Services_CategoryTBTest {

    private static final id = "1"

    @InjectMocks
    privateService_CategoryTB_impl service;

    @Mock
    private 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));

    }
    
}

The above test, using plain Mockito, will run much faster and tests exactly the same.

Next you also seem to be really keen on using @DirtiesContext don't as that will also slow down your tests as it will re-load the context each time it needs. As you are using mocks nothing will stick and thus no need to do a reload. So ditch them (and if something really needs it be very careful with it or even modify your test so you don't need it, like do cleanup after the test instead of reload the whole application).

CodePudding user response:

You could specify a JVM arg for your tests allowing for a higher memory usage like this in your pom.xml:

</project>    
    [...]
    <build>
      [...]
      <plugins>
        <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-surefire-plugin</artifactId>
           <version>2.22.2</version>
           <configuration>
                <argLine>-Xmx1024M</argLine> // You can provide comma separated values if you have more than one
           </configuration>
         </plugin>
       </plugins>
       [...]
    </build>
    [...]
</project>

If you specify enough memory, you shouldn't run into java.lang.OutOfMemoryErrors anymore.

  • Related