Search code examples
javaspringspring-bootunit-testingjunit

How to implement generated ApplicationTests in Spring Boot?


There is a generated test class as shown below:


@SpringBootTest
class ProductApplicationTests {

    @Test
    void contextLoads() {
    }
}

When I leave this, it causes error when I run my unit tests (some errors regarding to not connecting to the database). So, should I delete this class or how to implement it?


Solution

  • The @SpringBootTest annotation is for running integration tests. It will create a running application and test it like it would happen in a running instance of your application. As such, you will need to either deactivate the parts of your application that connect to databases/brokers/dependencies (either by mocking them with @MockBean or placing them behind configuration flags), or you will need to provide those dependencies and databases.

    Generally, it is preferable to alter the program as little as possible when running your tests, so there is a way to create a temporary instance of a database when launching this test, called Testcontainers. Here is a list of databases supported out of the box (the library is extensible too) - https://www.testcontainers.org/modules/databases/ . If you run into issues using it, there is a slack workspace for it, or just create new questions for them.

    You can also change the annotation from @SpringBootTest to @WebMvcTest. The spring guides - https://spring.io/guides/gs/testing-web/ - describe this. Basically, it won't try to connect to the database:

    In this test, the full Spring application context is started but without the server. We can narrow the tests to only the web layer by using @WebMvcTest, as the following listing (from src/test/java/com/example/testingweb/WebLayerTest.java) shows:

    by narrowing the tests, it is no longer a full integration test, but rather testing parts like a unit test. it also means it doesn't "create" the bottom part of your application (connection to the database, service beans, etc).


    For example, for this application:

    package org.example;
    
    import lombok.Data;
    import lombok.RequiredArgsConstructor;
    import lombok.SneakyThrows;
    import lombok.experimental.Accessors;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.ApplicationRunner;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.data.annotation.Id;
    import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
    import org.springframework.data.relational.core.mapping.Column;
    import org.springframework.data.relational.core.mapping.Table;
    import org.springframework.data.repository.CrudRepository;
    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Component;
    import org.springframework.stereotype.Repository;
    import org.springframework.stereotype.Service;
    import org.springframework.util.CollectionUtils;
    import org.springframework.web.bind.annotation.*;
    import org.springframework.web.server.ResponseStatusException;
    
    import javax.sql.DataSource;
    import java.sql.Connection;
    import java.sql.Statement;
    import java.util.Arrays;
    import java.util.Collections;
    import java.util.List;
    import java.util.Optional;
    
    
    @EnableJdbcRepositories(considerNestedRepositories = true)
    @SpringBootApplication
    public class ReservedNameApplication {
        public static void main(String[] args) {
            /*
                docker run -d --name local-postgres -p 5432:5432 \
                        -e POSTGRES_HOST_AUTH_METHOD=trust \
                        postgres:15-alpine
            */
            System.setProperty("spring.datasource.url",
                    "jdbc:postgresql://localhost/postgres");
            System.setProperty("spring.datasource.username", "postgres");
            SpringApplication.run(ReservedNameApplication.class, args);
        }
    
        @Autowired
        DataSource dataSource;
    
        @SneakyThrows
        @Bean
        ApplicationRunner createTables() {
            return args -> {
                try (Connection connection = dataSource.getConnection()) {
                    try (Statement statement = connection.createStatement()) {
                        statement.execute("""
                            create table if not exists hero
                            (
                                id serial,
                                name varchar(100) not null unique
                            );
                            """);
                    }
                }
            };
        }
    
        @Repository
        interface HeroRepository extends CrudRepository<Hero, Long> {
            Optional<Hero> findByName(String name);
        }
    
        @Accessors(chain = true)
        @Data
        @Table("hero")
        public static class Hero {
            @Id
            Long id;
            @Column("name")
            String name;
        }
    
        @Accessors(chain = true)
        @Data
        @Component
        @ConfigurationProperties(prefix = "hero.name-config")
        public static class HeroNameConfig {
            List<String> reservedNames = Arrays.asList("admin", "administrator");
        }
    
        @RequiredArgsConstructor
        @Service
        public static class HeroService {
            private final HeroRepository heroRepository;
            private final HeroNameConfig heroNameConfig;
    
            public Hero getHero(String name) {
                return heroRepository.findByName(name).orElse(null);
            }
    
            public Hero createHero(String name) {
                if (CollectionUtils.containsAny(heroNameConfig.getReservedNames(),
                        Collections.singleton(name))) {
                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                            "pick a different name");
                }
    
                return heroRepository.save(new Hero().setName(name));
            }
        }
    
        @RequiredArgsConstructor
        @RestController
        @RequestMapping("/api/hero")
        public static class HeroController {
            private final HeroService heroService;
    
            @GetMapping("/{name}")
            Hero getHero(@PathVariable String name) {
                return Optional.ofNullable(heroService.getHero(name))
                        .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
            }
    
            @PostMapping("/{name}")
            Hero createHero(@PathVariable String name) {
                return heroService.createHero(name);
            }
        }
    }
    

    here is how you might write an @SpringBootTest for it:

    package org.example;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.util.TestPropertyValues;
    import org.springframework.context.ApplicationContextInitializer;
    import org.springframework.context.ConfigurableApplicationContext;
    import org.springframework.lang.NonNull;
    import org.springframework.test.context.ActiveProfiles;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.web.reactive.server.WebTestClient;
    import org.testcontainers.containers.PostgreSQLContainer;
    import org.testcontainers.utility.DockerImageName;
    
    import java.util.Map;
    
    
    // tell spring to create a web server for the test
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    // we want the web test client to have a long timeout
    // to debug server side code without failing the test due to timeout
    @AutoConfigureWebTestClient(timeout = "PT24H")
    // for adding an itest-specific configuration file if you need to
    @ActiveProfiles("itest")
    // setting up our integration test application context
    @ContextConfiguration(initializers = ReservedNameApplicationITest.Init.class)
    class ReservedNameApplicationITest {
    
        @Autowired
        WebTestClient webTestClient;
    
        @Test
        void test_works() {
            webTestClient.get().uri("/api/hero/itest").exchange()
                    .expectStatus().isNotFound();
            webTestClient.post().uri("/api/hero/itest").exchange();
            webTestClient.get().uri("/api/hero/itest").exchange()
                    .expectStatus().isOk();
        }
    
        @Test
        void test_reservedWordFails() {
            webTestClient.get().uri("/api/hero/admin").exchange()
                    .expectStatus().isNotFound();
            webTestClient.post().uri("/api/hero/admin").exchange()
                    .expectStatus().isBadRequest();
        }
    
        static class Init
                implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    
            @Override
            public void initialize(@NonNull ConfigurableApplicationContext ctx) {
                var tag = DockerImageName.parse("postgres")
                        // use alpine for smaller image sizes
                        .withTag("15-alpine");
                // we trust Testcontainers to do its thing with the resource
                //noinspection resource
                PostgreSQLContainer<?> p = new PostgreSQLContainer<>(tag);
                p.start();
    
                TestPropertyValues.of(Map.of(
                        "spring.datasource.url", p.getJdbcUrl(),
                        "spring.datasource.username", p.getUsername(),
                        "spring.datasource.password", p.getPassword()
                )).applyTo(ctx.getEnvironment());
            }
        }
    
    }
    

    a complete example can be found here