Перейти к основному содержимому

Тестирование Spring Boot @ConfigurationProperties

· 7 мин. чтения

1. Обзор

В нашем предыдущем руководстве по @ConfigurationProperties мы узнали , как настроить и использовать аннотацию @ConfigurationProperties с Spring Boot для работы с внешней конфигурацией.

В этом руководстве мы обсудим, как тестировать классы конфигурации, которые полагаются на аннотацию @ConfigurationProperties , чтобы убедиться, что наши данные конфигурации загружены и правильно связаны с соответствующими полями.

2. Зависимости

В нашем проекте Maven мы будем использовать зависимости spring-boot-starter и spring-boot-starter-test , чтобы включить основной API Spring и тестовый API Spring. Кроме того, мы будем использовать spring-boot-starter-validation в качестве зависимости проверки бина:

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</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-validation</artifactId>
</dependency>

3. Привязка свойств к пользовательским объектам POJO

При работе с внешней конфигурацией мы обычно создаем объекты POJO, содержащие поля, соответствующие соответствующим свойствам конфигурации . Как мы уже знаем, Spring затем автоматически привяжет свойства конфигурации к классам Java, которые мы создаем.

Для начала предположим, что у нас есть некоторая конфигурация сервера внутри файла свойств с именем src/test/resources/server-config-test.properties :

server.address.ip=192.168.0.1
server.resources_path.imgs=/root/imgs

Мы определим простой класс конфигурации, соответствующий предыдущему файлу свойств:

@Configuration
@ConfigurationProperties(prefix = "server")
public class ServerConfig {

private Address address;
private Map<String, String> resourcesPath;

// getters and setters
}

А также соответствующий тип адреса :

public class Address {

private String ip;

// getters and setters
}

Наконец, мы добавим POJO ServerConfig в наш тестовый класс и проверим правильность установки всех его полей:

@ExtendWith(SpringExtension.class)
@EnableConfigurationProperties(value = ServerConfig.class)
@TestPropertySource("classpath:server-config-test.properties")
public class BindingPropertiesToUserDefinedPOJOUnitTest {

@Autowired
private ServerConfig serverConfig;

@Test
void givenUserDefinedPOJO_whenBindingPropertiesFile_thenAllFieldsAreSet() {
assertEquals("192.168.0.1", serverConfig.getAddress().getIp());

Map<String, String> expectedResourcesPath = new HashMap<>();
expectedResourcesPath.put("imgs", "/root/imgs");
assertEquals(expectedResourcesPath, serverConfig.getResourcesPath());
}
}

В этом тесте мы использовали следующие аннотации:

  • @ExtendWith — интегрирует платформу Spring TestContext с JUnit5.
  • @EnableConfigurationProperties — включает поддержку bean-компонентов @ConfigurationProperties (в данном случае bean-компонента ServerConfig )
  • @TestPropertySource — указывает файл тестирования, который переопределяет файл application.properties по умолчанию.

4. @ConfigurationProperties в методах @Bean

Другой способ создания компонентов конфигурации — использование аннотации @ConfigurationProperties для методов @Bean .

Например, следующий метод getDefaultConfigs() создает bean-компонент конфигурации ServerConfig :

@Configuration
public class ServerConfigFactory {

@Bean(name = "default_bean")
@ConfigurationProperties(prefix = "server.default")
public ServerConfig getDefaultConfigs() {
return new ServerConfig();
}
}

Как мы видим, мы можем настроить экземпляр ServerConfig , используя @ConfigurationProperties в методе getDefaultConfigs() , без необходимости редактирования самого класса ServerConfig . Это может быть особенно полезно при работе с внешним сторонним классом, доступ к которому ограничен.

Далее мы определим образец внешнего свойства:

server.default.address.ip=192.168.0.2

Наконец, чтобы сообщить Spring использовать класс ServerConfigFactory при загрузке ApplicationContext (таким образом создавая наш компонент конфигурации), мы добавим аннотацию @ContextConfiguration к тестовому классу:

@ExtendWith(SpringExtension.class)
@EnableConfigurationProperties(value = ServerConfig.class)
@ContextConfiguration(classes = ServerConfigFactory.class)
@TestPropertySource("classpath:server-config-test.properties")
public class BindingPropertiesToBeanMethodsUnitTest {

@Autowired
@Qualifier("default_bean")
private ServerConfig serverConfig;

@Test
void givenBeanAnnotatedMethod_whenBindingProperties_thenAllFieldsAreSet() {
assertEquals("192.168.0.2", serverConfig.getAddress().getIp());

// other assertions...
}
}

5. Проверка свойств

Чтобы включить проверку компонентов в Spring Boot, мы должны аннотировать класс верхнего уровня с помощью @Validated . Затем мы добавляем необходимые ограничения javax.validation :

@Configuration
@ConfigurationProperties(prefix = "validate")
@Validated
public class MailServer {

@NotNull
@NotEmpty
private Map<String, @NotBlank String> propertiesMap;

@Valid
private MailConfig mailConfig = new MailConfig();

// getters and setters
}

Точно так же класс MailConfig также имеет некоторые ограничения:

public class MailConfig {

@NotBlank
@Email
private String address;

// getters and setters
}

Предоставляя действительный набор данных:

validate.propertiesMap.first=prop1
validate.propertiesMap.second=prop2
validate.mail_config.address=user1@test

приложение запустится нормально, и наши модульные тесты пройдут:

@ExtendWith(SpringExtension.class)
@EnableConfigurationProperties(value = MailServer.class)
@TestPropertySource("classpath:property-validation-test.properties")
public class PropertyValidationUnitTest {

@Autowired
private MailServer mailServer;

private static Validator propertyValidator;

@BeforeAll
public static void setup() {
propertyValidator = Validation.buildDefaultValidatorFactory().getValidator();
}

@Test
void whenBindingPropertiesToValidatedBeans_thenConstrainsAreChecked() {
assertEquals(0, propertyValidator.validate(mailServer.getPropertiesMap()).size());
assertEquals(0, propertyValidator.validate(mailServer.getMailConfig()).size());
}
}

И наоборот, если мы используем недопустимые свойства, Spring выдаст исключение IllegalStateException при запуске .

Например, используя любую из этих недопустимых конфигураций:

validate.propertiesMap.second=
validate.mail_config.address=user1.test

приведет к сбою нашего приложения с этим сообщением об ошибке:

Property: validate.propertiesMap[second]
Value:
Reason: must not be blank

Property: validate.mailConfig.address
Value: user1.test
Reason: must be a well-formed email address

Обратите внимание, что мы использовали @Valid в поле mailConfig , чтобы убедиться, что ограничения MailConfig проверяются, даже если validate.mailConfig.address не определен. В противном случае Spring установит для mailConfig значение null и запустит приложение в обычном режиме.

6. Преобразование свойств

Преобразование свойств Spring Boot позволяет нам преобразовать некоторые свойства в определенные типы.

В этом разделе мы начнем с тестирования классов конфигурации, использующих встроенное преобразование Spring. Затем мы протестируем собственный конвертер, который создадим сами.

6.1. Преобразование Spring Boot по умолчанию

Рассмотрим следующие свойства размера и продолжительности данных:

# data sizes
convert.upload_speed=500MB
convert.download_speed=10

# durations
convert.backup_day=1d
convert.backup_hour=8

Spring Boot автоматически привяжет эти свойства к соответствующим полям DataSize и Duration , определенным в классе конфигурации PropertyConversion :

@Configuration
@ConfigurationProperties(prefix = "convert")
public class PropertyConversion {

private DataSize uploadSpeed;

@DataSizeUnit(DataUnit.GIGABYTES)
private DataSize downloadSpeed;

private Duration backupDay;

@DurationUnit(ChronoUnit.HOURS)
private Duration backupHour;

// getters and setters
}

Мы проверим результаты конвертации:

@ExtendWith(SpringExtension.class)
@EnableConfigurationProperties(value = PropertyConversion.class)
@ContextConfiguration(classes = CustomCredentialsConverter.class)
@TestPropertySource("classpath:spring-conversion-test.properties")
public class SpringPropertiesConversionUnitTest {

@Autowired
private PropertyConversion propertyConversion;

@Test
void whenUsingSpringDefaultSizeConversion_thenDataSizeObjectIsSet() {
assertEquals(DataSize.ofMegabytes(500), propertyConversion.getUploadSpeed());
assertEquals(DataSize.ofGigabytes(10), propertyConversion.getDownloadSpeed());
}

@Test
void whenUsingSpringDefaultDurationConversion_thenDurationObjectIsSet() {
assertEquals(Duration.ofDays(1), propertyConversion.getBackupDay());
assertEquals(Duration.ofHours(8), propertyConversion.getBackupHour());
}
}

6.2. Пользовательские конвертеры

Теперь давайте представим, что мы хотим преобразовать свойство convert.credentials :

convert.credentials=user,123

в следующий класс Credential :

public class Credentials {

private String username;
private String password;

// getters and setters
}

Для этого мы можем реализовать собственный преобразователь:

@Component
@ConfigurationPropertiesBinding
public class CustomCredentialsConverter implements Converter<String, Credentials> {

@Override
public Credentials convert(String source) {
String[] data = source.split(",");
return new Credentials(data[0], data[1]);
}
}

Наконец, мы добавим поле Credentials в класс PropertyConversion :

public class PropertyConversion {
private Credentials credentials;
// ...
}

В нашем тестовом классе SpringPropertiesConversionUnitTest нам также нужно добавить @ContextConfiguration для регистрации пользовательского преобразователя в контексте Spring:

// other annotations
@ContextConfiguration(classes=CustomCredentialsConverter.class)
public class SpringPropertiesConversionUnitTest {

//...

@Test
void whenRegisteringCustomCredentialsConverter_thenCredentialsAreParsed() {
assertEquals("user", propertyConversion.getCredentials().getUsername());
assertEquals("123", propertyConversion.getCredentials().getPassword());
}
}

Как показывают предыдущие утверждения, Spring использовал наш пользовательский конвертер для разбора свойства convert.credentials в экземпляр Credentials .

7. Привязка документов YAML

Для иерархических данных конфигурации конфигурация YAML может быть более удобной. YAML также поддерживает определение нескольких профилей в одном документе.

Следующий файл application.yml, расположенный в папке src/test/resources/ , определяет «тестовый» профиль для класса ServerConfig :

spring:
config:
activate:
on-profile: test
server:
address:
ip: 192.168.0.4
resources_path:
imgs: /etc/test/imgs
---
# other profiles

В результате будет пройден следующий тест:

@ExtendWith(SpringExtension.class)
@ContextConfiguration(initializers = ConfigDataApplicationContextInitializer.class)
@EnableConfigurationProperties(value = ServerConfig.class)
@ActiveProfiles("test")
public class BindingYMLPropertiesUnitTest {

@Autowired
private ServerConfig serverConfig;

@Test
void whenBindingYMLConfigFile_thenAllFieldsAreSet() {
assertEquals("192.168.0.4", serverConfig.getAddress().getIp());

// other assertions ...
}
}

Несколько замечаний относительно аннотаций, которые мы использовали:

  • @ContextConfiguration(initializers = ConfigDataApplicationContextInitializer.class ) — загружает файл application.yml
  • @ActiveProfiles("test") - указывает, что во время этого теста будет использоваться профиль "test"

Наконец, имейте в виду, что ни @ProperySource , ни @TestProperySource не поддерживают загрузку файлов .yml . Поэтому мы всегда должны размещать наши конфигурации YAML в файле application.yml .

8. Переопределение конфигураций @ConfigurationProperties

Иногда нам может понадобиться переопределить свойства конфигурации, загруженные @ConfigurationProperties , с другим набором данных, особенно при тестировании.

Как мы видели в предыдущих примерах, мы можем использовать @TestPropertySource("path_to_new_data_set") , чтобы заменить всю исходную конфигурацию (в /src/main/resources) новой.

В качестве альтернативы мы могли бы выборочно заменить некоторые исходные свойства, используя атрибут properties @TestPropertySource .

Предположим, мы хотим переопределить ранее определенное свойство validate.mail_config.address другим значением. Все, что нам нужно сделать, это аннотировать наш тестовый класс с помощью @TestPropertySource, а затем присвоить новое значение тому же свойству через список свойств :

@TestPropertySource(properties = {"validate.mail_config.address=new_user@test"})

Следовательно, Spring будет использовать вновь определенное значение:

assertEquals("new_user@test", mailServer.getMailConfig().getAddress());

9. Заключение

В этой статье мы узнали, как тестировать различные типы классов конфигурации, которые используют аннотацию @ConfigurationProperties для загрузки файлов конфигурации .properties и .yml .

Как обычно, исходный код этой статьи доступен на GitHub .