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와 같은 기술을 사용하는 경우 테스트 워크플로는 다음과 같습니다.

  1. 모든 테스트 전에 PostgreSQL과 같은 구성 요소를 설정합니다. 일반적으로 이러한 구성 요소는 랜덤의 포트를 수신합니다.
  2. 테스트를 실행합니다.
  3. 구성 요소를 분해합니다.

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 컴포넌트를 테스트하는 것이 얼마나 어려운지 보았습니다. 그런 다음 이 문제에 대한 세 가지 솔루션을 도입했으며 각각 이전 솔루션이 제공해야 했던 기능을 개선했습니다.

평소와 같이 모든 예제는 GitHub에서 사용할 수 있습니다 .

Generic footer banner