Статьи

Тестирование Spring Boot обусловливает здравый смысл

Если вы более или менее опытный пользователь Spring Boot , очень удачно, что в какой-то момент вам, возможно, придется столкнуться с ситуацией, когда конкретные бины или конфигурации должны быть введены условно . Механика этого хорошо понятна, но иногда тестирование таких условий (и их комбинаций) может привести к путанице. В этом посте мы поговорим о некоторых возможных (возможно, вменяемых) способах решения этой проблемы.

Поскольку Spring Boot 1.5.x по-прежнему широко используется (тем не менее он движется в направлении EOL в августе этого года ), мы бы включили его вместе с Spring Boot 2.1.x , как с JUnit 4.x, так и с JUnit 5.x. Методы, которые мы собираемся охватить, в равной степени применимы как к обычным классам конфигурации, так и к классам автоконфигурации .

Пример, с которым мы будем играть, будет связан с нашей самодельной регистрацией. Предположим, что нашему приложению Spring Boot требуется некоторый компонент для выделенного регистратора с именем «sample» . Однако в определенных обстоятельствах этот регистратор должен быть отключен (или фактически превращен в noop), поэтому свойство logging.enabled служит здесь как переключатель уничтожения. В этом примере мы используем Slf4j и Logback , но это не очень важно. Фрагмент LoggingConfiguration ниже отражает эту идею.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Configuration
public class LoggingConfiguration {
    @Configuration
    @ConditionalOnProperty(name = "logging.enabled", matchIfMissing = true)
    public static class Slf4jConfiguration {
        @Bean
        Logger logger() {
            return LoggerFactory.getLogger("sample");
        }
    }
     
    @Bean
    @ConditionalOnMissingBean
    Logger logger() {
        return new NOPLoggerFactory().getLogger("sample");
    }
}

Так как бы мы это проверили? Spring BootSpring Framework в целом) всегда предлагали выдающуюся поддержку лесов для тестирования . Аннотации @SpringBootTest и @TestPropertySource позволяют быстро загрузить контекст приложения с настроенными свойствами. Однако есть одна проблема: они применяются на уровне класса теста, а не на метод теста. Это, конечно, имеет смысл, но в основном требует создания тестового класса для каждой комбинации условий.

Если вы все еще работаете с JUnit 4.x , вам может пригодиться один трюк, который использует скрытый бегун, скрытую драгоценность фреймворка.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@RunWith(Enclosed.class)
public class LoggingConfigurationTest {
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public static class LoggerEnabledTest {
        @Autowired private Logger logger;
         
        @Test
        public void loggerShouldBeSlf4j() {
            assertThat(logger).isInstanceOf(ch.qos.logback.classic.Logger.class);
        }
    }
     
    @RunWith(SpringRunner.class)
    @SpringBootTest
    @TestPropertySource(properties = "logging.enabled=false")
    public static class LoggerDisabledTest {
        @Autowired private Logger logger;
         
        @Test
        public void loggerShouldBeNoop() {
            assertThat(logger).isSameAs(NOPLogger.NOP_LOGGER);
        }
    }
}

У вас все еще есть класс для каждого условия, но по крайней мере они все находятся в одном гнезде. С JUnit 5.x некоторые вещи стали проще, но не до уровня, который можно было бы ожидать. К сожалению, Spring Boot 1.5.x изначально не поддерживает JUnit 5.x , поэтому мы должны полагаться на расширение, предоставляемое модулем сообщества spring-test-junit5 . Вот соответствующие изменения в pom.xml , обратите внимание, что junit явно исключен из графика зависимостей spring-boot-starter-test .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </exclusion>
    </exclusions>
</dependency>
 
<dependency>
    <groupId>com.github.sbrannen</groupId>
    <artifactId>spring-test-junit5</artifactId>
    <version>1.5.0</version>
    <scope>test</scope>
</dependency>
 
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.5.0</version>
    <scope>test</scope>
</dependency>
 
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.5.0</version>
    <scope>test</scope>
</dependency>

Сам контрольный пример не сильно отличается от использования аннотации @Nested , которая поставляется из JUnit 5.x для поддержки тестов как внутренних классов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class LoggingConfigurationTest {
    @Nested
    @ExtendWith(SpringExtension.class)
    @SpringBootTest
    @DisplayName("Logging is enabled, expecting Slf4j logger")
    public static class LoggerEnabledTest {
        @Autowired private Logger logger;
         
        @Test
        public void loggerShouldBeSlf4j() {
            assertThat(logger).isInstanceOf(ch.qos.logback.classic.Logger.class);
        }
    }
     
    @Nested
    @ExtendWith(SpringExtension.class)
    @SpringBootTest
    @TestPropertySource(properties = "logging.enabled=false")
    @DisplayName("Logging is disabled, expecting NOOP logger")
    public static class LoggerDisabledTest {
        @Autowired private Logger logger;
         
        @Test
        public void loggerShouldBeNoop() {
            assertThat(logger).isSameAs(NOPLogger.NOP_LOGGER);
        }
    }
}

Если вы попытаетесь запустить тесты из командной строки с помощью плагинов Apache Maven и Maven Surefire , вы можете быть удивлены, увидев, что ни один из них не был выполнен во время сборки. Проблема в том, что … все вложенные классы исключены … поэтому нам нужно найти другой обходной путь .

01
02
03
04
05
06
07
08
09
10
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.22.2</version>
    <configuration>
        <excludes>
            <exclude />
        </excludes>
    </configuration>
</plugin>

При этом все должно идти гладко. Но хватит о наследии , Spring Boot 2.1.x полностью меняет игру. Семейство контекстных бегунов, ApplicationContextRunner , ReactiveWebApplicationContextRunner и WebApplicationContextRunner , предоставляют простой и прямой способ настройки контекста на уровне метода тестирования, поддерживая невероятно быстрое выполнение теста.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public class LoggingConfigurationTest {
    private final ApplicationContextRunner runner = new ApplicationContextRunner()
        .withConfiguration(UserConfigurations.of(LoggingConfiguration.class));
     
    @Test
    public void loggerShouldBeSlf4j() {
        runner
            .run(ctx ->
                assertThat(ctx.getBean(Logger.class)).isInstanceOf(Logger.class)
            );
    }
     
    @Test
    public void loggerShouldBeNoop() {
        runner
            .withPropertyValues("logging.enabled=false")
            .run(ctx ->
                assertThat(ctx.getBean(Logger.class)).isSameAs(NOPLogger.NOP_LOGGER)
            );
    }
}

Это выглядит действительно здорово. Поддержка JUnit 5.x в Spring Boot 2.1.x стала намного лучше, а с выходом 2.2 релиз, JUnit 5.x будет механизмом по умолчанию (не волнуйтесь, старый JUnit 4.x по- прежнему будет поддерживаться). На данный момент переход на JUnit 5.x требует немного работы на стороне зависимостей.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </exclusion>
    </exclusions>
</dependency>
 
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <scope>test</scope>
</dependency>
 
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <scope>test</scope>
</dependency>

В качестве дополнительного шага вам может понадобиться использовать последний плагин Maven Surefire , версии 2.22.0 или выше, с готовой поддержкой JUnit 5.x. Фрагмент ниже иллюстрирует это.

1
2
3
4
5
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.22.2</version>
</plugin>

Пример конфигурации, с которым мы работали, довольно наивен, многие из реальных приложений заканчиваются довольно сложным контекстом, построенным из многих условий. Гибкость и огромные возможности, которые открывают раннеры контекста, бесценное дополнение к тестовым лесам Spring Boot 2.x , — просто живые заставки, имейте это в виду.

Полные источники проекта доступны на Github .

Опубликовано на Java Code Geeks с разрешения Андрея Редько, партнера нашей программы JCG . Посмотрите оригинальную статью здесь: Тестирование Spring Boot обусловливает здравый смысл

Мнения, высказанные участниками Java Code Geeks, являются их собственными.