1. 개요
오늘날의 애플리케이션은 따로 떨어져 있지 않습니다. 일반적으로 PostgreSQL, Apache Kafka, Cassandra, Redis 및 기타 외부 API와 같은 다양한 외부 구성 요소에 연결해야 합니다.
이 예제에서는 Spring Framework 5.2.5가 동적 속성을 도입하여 이러한 애플리케이션을 테스트하는 것을 어떻게 용이하게 하는지 볼 것 입니다.
먼저 문제를 정의하고 이상적이지 않은 방식으로 문제를 해결하는 데 사용한 방법을 살펴보는 것으로 시작하겠습니다. 그런 다음 @DynamicPropertySource 어노테이션을 소개 하고 동일한 문제에 대해 더 나은 솔루션을 제공하는 방법을 살펴보겠습니다. 마지막으로 순수한 Spring 솔루션과 비교하여 월등할 수 있는 테스트 프레임워크의 다른 솔루션도 살펴보겠습니다.
2. 문제: 동적 속성
PostgreSQL을 데이터베이스로 사용하는 일반적인 애플리케이션을 개발한다고 가정해 봅시다. 간단한 JPA 엔터티로 시작하겠습니다 .
@Entity
@Table(name = "articles")
public class Article {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
private String title;
private String content;
// getters and setters
}
이 엔터티가 예상대로 작동하는지 확인하려면 데이터베이스 상호 작용을 확인하는 테스트를 작성해야 합니다. 이 테스트는 실제 데이터베이스와 통신해야 하므로 사전에 PostgreSQL 인스턴스를 설정해야 합니다.
테스트 실행 중에 이러한 인프라 도구를 설정하는 다양한 접근 방식 이 있습니다 . 실제로 이러한 솔루션에는 세 가지 주요 범주가 있습니다.
- 테스트를 위해 어딘가에 별도의 데이터베이스 서버를 설정하십시오.
- H2와 같은 경량의 테스트 전용 대안이나 가짜를 사용하십시오.
- 테스트 자체가 데이터베이스의 수명 주기를 관리하도록 합니다.
테스트 환경과 프로덕션 환경을 구분해서는 안 되므로 H2 와 같은 이중 테스트 를 사용하는 것보다 더 나은 대안이 있습니다 . 세 번째 옵션은 실제 데이터베이스로 작업하는 것 외에도 테스트에 대해 더 나은 격리를 제공합니다 . 또한 Docker 및 Testcontainers 와 같은 기술 을 사용하면 세 번째 옵션을 쉽게 구현할 수 있습니다.
Testcontainers와 같은 기술을 사용하는 경우 테스트 워크플로는 다음과 같습니다.
- 모든 테스트 전에 PostgreSQL과 같은 구성 요소를 설정합니다. 일반적으로 이러한 구성 요소는 랜덤의 포트를 수신합니다.
- 테스트를 실행합니다.
- 구성 요소를 분해합니다.
PostgreSQL 컨테이너가 매번 랜덤의 포트를 수신할 예정이라면 어떻게든 spring.datasource.url 구성 속성을 동적으로 설정하고 변경해야 합니다 . 기본적으로 각 테스트에는 해당 구성 속성의 고유한 버전이 있어야 합니다.
구성이 정적인 경우 Spring Boot의 구성 관리 기능을 사용하여 쉽게 관리할 수 있습니다 . 그러나 동적 구성에 직면했을 때 동일한 작업이 어려울 수 있습니다.
이제 문제를 알았으므로 이에 대한 기존 솔루션을 살펴보겠습니다.
3. 기존 솔루션
동적 속성을 구현하는 첫 번째 방법은 사용자 지정 ApplicationContextInitializer 를 사용하는 것 입니다. 기본적으로 먼저 인프라를 설정하고 첫 번째 단계의 정보를 사용하여 ApplicationContext 를 사용자 지정합니다 .
@SpringBootTest
@Testcontainers
@ContextConfiguration(initializers = ArticleTraditionalLiveTest.EnvInitializer.class)
class ArticleTraditionalLiveTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:11")
.withDatabaseName("prop")
.withUsername("postgres")
.withPassword("pass")
.withExposedPorts(5432);
static class EnvInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
TestPropertyValues.of(
String.format("spring.datasource.url=jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort()),
"spring.datasource.username=postgres",
"spring.datasource.password=pass"
).applyTo(applicationContext);
}
}
// omitted
}
이 다소 복잡한 설정을 살펴보겠습니다. JUnit은 다른 무엇보다 먼저 컨테이너를 만들고 시작합니다. 컨테이너가 준비되면 Spring 확장은 이니셜라이저를 호출하여 동적 구성을 Spring Environment 에 적용합니다 . 분명히 이 접근 방식은 다소 장황하고 복잡합니다.
다음 단계를 거쳐야 테스트를 작성할 수 있습니다.
@Autowired
private ArticleRepository articleRepository;
@Test
void givenAnArticle_whenPersisted_thenShouldBeAbleToReadIt() {
Article article = new Article();
article.setTitle("A Guide to @DynamicPropertySource in Spring");
article.setContent("Today's applications...");
articleRepository.save(article);
Article persisted = articleRepository.findAll().get(0);
assertThat(persisted.getId()).isNotNull();
assertThat(persisted.getTitle()).isEqualTo("A Guide to @DynamicPropertySource in Spring");
assertThat(persisted.getContent()).isEqualTo("Today's applications...");
}
4. @DynamicPropertySource
Spring Framework 5.2.5는 @DynamicPropertySource 어노테이션을 도입하여 동적 값으로 속성을 추가하는 것을 용이하게 합니다 . 우리가 해야 할 일은 @DynamicPropertySource 어노테이션이 달린 정적 메서드를 만들고 입력으로 단일 DynamicPropertyRegistry 인스턴스 만 갖는 것입니다 .
@SpringBootTest
@Testcontainers
public class ArticleLiveTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:11")
.withDatabaseName("prop")
.withUsername("postgres")
.withPassword("pass")
.withExposedPorts(5432);
@DynamicPropertySource
static void registerPgProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url",
() -> String.format("jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort()));
registry.add("spring.datasource.username", () -> "postgres");
registry.add("spring.datasource.password", () -> "pass");
}
// tests are same as before
}
위에 표시된 것처럼 주어진 DynamicPropertyRegistry 에서 add(String, Supplier<Object>) 메서드 를 사용하여 Spring Environment에 일부 속성을 추가 합니다. 이 접근 방식은 이전에 본 초기화 프로그램에 비해 훨씬 깨끗합니다. @DynamicPropertySource 어노테이션이 있는 메서드 는 정적 으로 선언되어야 하며 DynamicPropertyRegistry 유형의 인수 하나만 허용해야 합니다 .
기본적으로 @DynmicPropertySource 어노테이션 의 주요 동기 는 이미 가능했던 것을 더 쉽게 촉진하는 것입니다. 처음에는 Testcontainers와 함께 작동하도록 설계되었지만 동적 구성으로 작업해야 하는 모든 곳에서 사용할 수 있습니다.
5. 대안: 테스트 설비
지금까지 두 접근 방식 모두 고정 장치 설정과 테스트 코드가 밀접하게 얽혀 있습니다. 때로는 두 가지 관심사의 긴밀한 결합으로 인해 테스트 코드가 복잡해집니다. 특히 설정할 항목이 여러 개인 경우에는 더욱 그렇습니다. 단일 테스트에서 PostgreSQL 및 Apache Kafka를 사용하는 경우 인프라 설정이 어떻게 보일지 상상해 보십시오.
그 외에도 인프라 설정 및 동적 구성 적용은 이를 필요로 하는 모든 테스트에서 복제됩니다 .
이러한 단점을 피하기 위해 대부분의 테스트 프레임워크에서 제공하는 테스트 픽스처 기능을 사용할 수 있습니다 . 예를 들어 JUnit 5에서는 모든 테스트 전에 PostgreSQL 인스턴스를 시작하고, Spring Boot를 구성하고, 테스트 실행 후 PostgreSQL 인스턴스를 중지 하는 확장 을 정의할 수 있습니다 .
public class PostgreSQLExtension implements BeforeAllCallback, AfterAllCallback {
private PostgreSQLContainer<?> postgres;
@Override
public void beforeAll(ExtensionContext context) {
postgres = new PostgreSQLContainer<>("postgres:11")
.withDatabaseName("prop")
.withUsername("postgres")
.withPassword("pass")
.withExposedPorts(5432);
postgres.start();
String jdbcUrl = String.format("jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort());
System.setProperty("spring.datasource.url", jdbcUrl);
System.setProperty("spring.datasource.username", "postgres");
System.setProperty("spring.datasource.password", "pass");
}
@Override
public void afterAll(ExtensionContext context) {
postgres.stop();
}
}
여기서는 AfterAllCallback 및 BeforeAllCallback 을 구현 하여 JUnit 5 확장을 생성합니다. 이런 식으로 JUnit 5는 모든 테스트를 실행하기 전에 beforeAll() 로직을 실행하고 테스트 를 실행한 후에 는 afterAll() 메소드 의 로직을 실행합니다. 이 접근 방식을 사용하면 테스트 코드가 다음과 같이 깨끗해집니다.
@SpringBootTest
@ExtendWith(PostgreSQLExtension.class)
public class ArticleTestFixtureLiveTest {
// just the test code
}
더 읽기 쉬워질 뿐만 아니라 @ExtendWith(PostgreSQLExtension.class) 어노테이션 을 추가하는 것만으로 동일한 기능을 쉽게 재사용할 수 있습니다 . 다른 두 접근 방식에서와 같이 필요한 모든 위치에 전체 PostgreSQL 설정을 복사하여 붙여넣을 필요가 없습니다.
6. 결론
이 예제에서 우리는 먼저 데이터베이스와 같은 것에 의존하는 Spring 컴포넌트를 테스트하는 것이 얼마나 어려운지 보았습니다. 그런 다음 이 문제에 대한 세 가지 솔루션을 도입했으며 각각 이전 솔루션이 제공해야 했던 기능을 개선했습니다.