๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
BackEnd๐ŸŒฑ/Spring

TestContainer๋กœ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์ถ•ํ•˜๊ธฐ

by ์•ˆ์ฃผํ˜• 2023. 10. 15.

[๊ฐœ์š”]

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•  ๋•Œ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€์˜ ์—ฐ๋™์€ ์ฃผ์š”ํ•œ ๊ณ ๋ ค์‚ฌํ•ญ ์ค‘ ํ•˜๋‚˜์ด๋ฉฐ, ํ…Œ์ŠคํŠธ์˜ ์•ˆ์ •์„ฑ๊ณผ ์‹ ๋ขฐ์„ฑ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด์„œ๋Š” ์‹ค์ œ ์šด์˜ ํ™˜๊ฒฝ๊ณผ ์œ ์‚ฌํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ™˜๊ฒฝ์—์„œ์˜ ํ…Œ์ŠคํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด๋Ÿฌํ•œ ํ™˜๊ฒฝ์„ ๊ฐ–์ถ”๊ธฐ ์œ„ํ•ด์„  ์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•์ด ์žˆ๊ณ , ๊ฐ๊ฐ์˜ ๋ฐฉ๋ฒ•์€ ๊ทธ ํŠน์„ฑ๊ณผ ์žฅ๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ ๊ฒŒ์‹œ๊ธ€์—์„œ๋Š” TestContainer๋ฅผ ์ด์šฉํ•œ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์ถ•์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์ „์ฒด ์ฝ”๋“œ๋Š” ๊นƒํ—ˆ๋ธŒ์—์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
 
 
 

[DB๋ฅผ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์— ํ†ตํ•ฉํ•˜๋Š” ๋ฐฉ๋ฒ•]

Local์— ์‹ค์ œ DB๋ฅผ ์—ฐ๊ฒฐํ•˜๊ธฐ

Local ํ™˜๊ฒฝ์— ์ง์ ‘ DB๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์‹ค์ œ ํ™˜๊ฒฝ๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ ํ…Œ์ŠคํŠธ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋Š” ํฐ ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์—ฌ๋Ÿฌ ํ…Œ์ŠคํŠธ๋ฅผ ๋™์‹œ์— ์ง„ํ–‰ํ•˜๊ฑฐ๋‚˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์ธก๋ฉด์—์„œ ๋ฉฑ๋“ฑ์„ฑ(idempotencty)์„ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์–ด๋ ต๊ณ , ํŠนํžˆ DDL๊ณผ ๊ด€๋ จ๋œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•  ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™”์™€ ๊ฐ™์€ ์ถ”๊ฐ€ ์ž‘์—…์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

in-memory DB ํ™œ์šฉํ•˜๊ธฐ

in-momory DB, ์˜ˆ๋ฅผ ๋“ค๋ฉด H2๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ํ…Œ์ŠคํŠธ์˜ ์†๋„๊ฐ€ ๋งค์šฐ ๋น ๋ฅด๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ORM ๋„๊ตฌ๋ฅผ ํ†ตํ•ด ํŠน์ • DB์˜ ์˜์กด์„ฑ์„ ์ค„์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ์‹ค์ œ DB์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํŠนํ™”๋œ ๊ธฐ๋Šฅ์ด๋‚˜ DDL์— ๋Œ€ํ•œ ์ œ์•ฝ์‚ฌํ•ญ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

Embedded Library ํ™œ์šฉํ•˜๊ธฐ

Embedded Library๋Š” ์‹ค์ œ DB์˜ ์ฝ”๋“œ๋ฅผ ๊ฒฝ๋Ÿ‰ํ™”ํ•œ ํ˜•ํƒœ๋กœ ์ œ๊ณตํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ ํ™œ์šฉํ•˜๋ฉด in-momory DB์—์„œ ๊ฒช๊ฒŒ ๋˜๋Š” ํŠน์ • DB์˜ ํŠนํ™”๋œ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ์˜ ์ œ์•ฝ์„ ๊ทน๋ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 

๊ทธ๋Ÿฌ๋‚˜ ๋ชจ๋“  DB๊ฐ€ Embedded Library๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š์œผ๋ฉฐ, ์ง€์›๋˜๋Š” ๋ฒ„์ „์ด๋‚˜ OS์— ๋”ฐ๋ผ ์ œํ•œ์ด ์žˆ์„ ์ˆ˜ ์žˆ์œผ๊ณ , ๊ฐ DB๋งˆ๋‹ค ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋งค๋ฒˆ ๋‹ค๋ฅด๊ฒŒ ๊ตฌํ˜„ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

https://dealicious-inc.github.io/2022/01/10/test-containers.html
https://dealicious-inc.github.io/2022/01/10/test-containers.html

TestContainer ํ™œ์šฉํ•˜๊ธฐ

TestContainer๋Š” Docker ๊ธฐ๋ฐ˜์˜ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•˜๋Š” ๋„๊ตฌ๋กœ, Docker Compose์™€ ์œ ์‚ฌํ•˜๊ฒŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์™ธ๋ถ€ ์„ค์ • ํŒŒ์ผ ์—†์ด๋„ Java ์ฝ”๋“œ๋งŒ์œผ๋กœ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ๊ฐ„ํŽธํ•˜๊ฒŒ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์€ ์‹ค์ œ ํ™˜๊ฒฝ๊ณผ ์œ ์‚ฌํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋™์‹œ์—, ์ปจํ…Œ์ด๋„ˆ ๊ฐ„์˜ ํ†ต์‹  ๋ฌธ์ œ๋‚˜ ์ดˆ๊ธฐํ™” ๊ด€๋ จ๋œ ๋ฌธ์ œ๋ฅผ ์‰ฝ๊ฒŒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. 

 

 

[TestContainer๋ž€?]

TestContainer๋Š” Java ํ”Œ๋žซํผ์— ํŠนํ™”๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ์„œ, ๋„์ปค ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๊ฐ„ํŽธํ•˜๊ฒŒ ๋‹ค๋ฃจ๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ฃผ๋œ ๋ชฉ์ ์€ JUnit ํ…Œ์ŠคํŠธ ์ค‘ ๋„์ปค ์ปจํ…Œ์ด๋„ˆ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ํ™œ์šฉํ•˜์—ฌ ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋‚˜ end-to-end ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ์™ธ๋ถ€ ์˜์กด์„ฑ ์š”์†Œ๊ฐ€ ํ•„์š”ํ•œ ์ƒํ™ฉ์€ ๋งค์šฐ ํ”ํ•œ๋ฐ, ์˜ˆ๋ฅผ ๋“ค๋ฉด (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ๋ฉ”์‹œ์ง• ์‹œ์Šคํ…œ, ์ธ๋ฉ”๋ชจ๋ฆฌ ์ €์žฅ์†Œ ๋“ฑ)์ด ์ด๋Ÿฐ ์š”์†Œ์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์„œ๋น„์Šค๋“ค์„ ๋งค๋ฒˆ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์— ์‹ค์ œ๋กœ ์„ค์น˜ํ•˜๊ณ  ๊ตฌ๋™ํ•˜๋Š” ๊ฒƒ์€ ์‹œ๊ฐ„์ ์œผ๋กœ ๋งŽ์ด ์†Œ๋ชจ๋˜๋ฉฐ, ํ™˜๊ฒฝ ๋ณต์žก์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด TestContainer๋Š” ๋„์ปค ์ปจํ…Œ์ด๋„ˆ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํ•„์š”ํ•œ ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ๊ฐ€์ƒํ™”๋œ ํ™˜๊ฒฝ์—์„œ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ์‹ค์ œ ์„œ๋น„์Šค๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๋Œ€์‹  ํ•ด๋‹น ์„œ๋น„์Šค์˜ ๋„์ปค ์ด๋ฏธ์ง€๋ฅผ ๋น ๋ฅด๊ฒŒ ์‹คํ–‰ํ•˜๊ณ  ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚˜๋ฉด ์†์‰ฝ๊ฒŒ ์ข…๋ฃŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

[TestContainer๋ฅผ ์ด์šฉํ•œ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์ถ•]

build.gradle

// TestContainer ๊ธฐ๋ณธ ์˜์กด์„ฑ
testImplementation "org.testcontainers:testcontainers:1.19.0"
// MySQL ์˜์กด์„ฑ
testImplementation 'org.testcontainers:mysql:1.16.0'
// Redis ์˜์กด์„ฑ
testImplementation "com.redis.testcontainers:testcontainers-redis-junit:1.6.4"
// Kafka ์˜์กด์„ฑ
testImplementation("org.testcontainers:kafka:1.17.6")

IntegrationTest.java

@Disabled
@SpringBootTest
@Transactional
@ContextConfiguration(initializers = IntegrationTest.IntegrationTestInitializer.class)
public class IntegrationTest {
    // ๊ฐ ์ปจํ…Œ์ด๋„ˆ ์ธ์Šคํ„ด์Šค ์ •์˜
    private static final MySQLContainer<?> MYSQL_CONTAINER = new MySQLContainer<>(DockerImageName.parse("mysql:8.0.26"))
            .withDatabaseName("container")
            .withUsername("user")
            .withPassword("password");
    private static final RedisContainer REDIS_CONTAINER = new RedisContainer(RedisContainer.DEFAULT_IMAGE_NAME.withTag("6"));
    private static final KafkaContainer KAFKA_CONTAINER = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.0"));

    @BeforeAll
    public static void setupContainers() {
        // MySQL ์ปจํ…Œ์ด๋„ˆ ์‹œ์ž‘
        MYSQL_CONTAINER.start();
        // Redis ์ปจํ…Œ์ด๋„ˆ ์‹œ์ž‘
        REDIS_CONTAINER.start();
        // Kafka ์ปจํ…Œ์ด๋„ˆ ์‹œ์ž‘
        KAFKA_CONTAINER.start();
    }

    static class IntegrationTestInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        @Override
        public void initialize(@NotNull ConfigurableApplicationContext applicationContext) {
            Map<String, String> properties = new HashMap<>();

            // ๊ฐ ์„œ๋น„์Šค์˜ ์—ฐ๊ฒฐ ์ •๋ณด ์„ค์ •
            setKafkaProperties(properties);
            setDatabaseProperties(properties);
            setRedisProperties(properties);

            // ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…์ŠคํŠธ์— ์†์„ฑ๊ฐ’ ์ ์šฉ
            TestPropertyValues.of(properties).applyTo(applicationContext);
        }

        // Kafka ์—ฐ๊ฒฐ ์ •๋ณด ์„ค์ •
        private void setKafkaProperties(Map<String, String> properties) {
            properties.put("spring.kafka.bootstrap-servers", KAFKA_CONTAINER.getBootstrapServers());
        }

        // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ •๋ณด ์„ค์ •
        private void setDatabaseProperties(Map<String, String> properties) {
            properties.put("spring.datasource.url", MYSQL_CONTAINER.getJdbcUrl());
            properties.put("spring.datasource.username", MYSQL_CONTAINER.getUsername());
            properties.put("spring.datasource.password", MYSQL_CONTAINER.getPassword());
        }

        // Redis ์—ฐ๊ฒฐ ์ •๋ณด ์„ค์ •
        private void setRedisProperties(Map<String, String> properties) {
            properties.put("spring.redis.host", REDIS_CONTAINER.getHost());
            properties.put("spring.redis.port", REDIS_CONTAINER.getFirstMappedPort().toString());
        }
    }
}

์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • '@Disabled': IntegrationTest ํด๋ž˜์Šค๋Š” ์ง์ ‘ ์‹คํ–‰๋˜์ง€ ์•Š๊ณ , ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค์—์„œ ์ƒ์†ํ•˜์—ฌ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ…Œ์ŠคํŠธ๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ž…๋‹ˆ๋‹ค.
  • '@ContextConfiguration': ์Šคํ”„๋ง ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…์ŠคํŠธ๋ฅผ ์„ค์ •ํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ž…๋‹ˆ๋‹ค. IntegrationTestInitializer ํด๋ž˜์Šค์—์„œ ์ •์˜ํ•œ ์ดˆ๊ธฐํ™” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  • 'IntegrationTestInitializer': ApplicationContesxtInitializer ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ ๋‚ด๋ถ€ ํด๋ž˜์Šค๋กœ, ์Šคํ”„๋ง ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…์ŠคํŠธ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.

 

Docker Compose๋ฅผ ์‚ฌ์šฉํ•œ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์„ค์ •

๋งŒ์•ฝ Docker Compose๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ DB๋ฅผ ์„ธํŒ…ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

IntegrationTest.java

@Disabled
@SpringBootTest
@Transactional
@ContextConfiguration(initializers = IntegrationTest.IntegrationTestInitializer.class)
public class IntegrationTest {

    // Docker Compose์™€ Kafka ์ปจํ…Œ์ด๋„ˆ ์ธ์Šคํ„ด์Šค ์ •์˜
    private static final DockerComposeContainer<?> DOCKER_COMPOSE =
            new DockerComposeContainer<>(new File("src/test/resources/docker-compose.yml"))
                    .withExposedService("mysql", 3306, Wait.forLogMessage(".*ready for connections.*", 1))
                    .withExposedService("redis", 6379, Wait.forLogMessage(".*Ready to accept connections.*", 1));

    private static final KafkaContainer KAFKA_CONTAINER =
            new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.0"));

    @BeforeAll
    public static void setupContainers() {
        DOCKER_COMPOSE.start();
        KAFKA_CONTAINER.start();
    }

    static class IntegrationTestInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        @Override
        public void initialize(@NotNull ConfigurableApplicationContext applicationContext) {
            Map<String, String> properties = new HashMap<>();

            setKafkaProperties(properties);
            setDatabaseProperties(properties);
            setRedisProperties(properties);

            TestPropertyValues.of(properties).applyTo(applicationContext);
        }

        private void setKafkaProperties(Map<String, String> properties) {
            properties.put("spring.kafka.bootstrap-servers", KAFKA_CONTAINER.getBootstrapServers());
        }

        private void setDatabaseProperties(Map<String, String> properties) {
            String rdbmsHost = DOCKER_COMPOSE.getServiceHost("mysql", 3306);
            int rdbmsPort = DOCKER_COMPOSE.getServicePort("mysql", 3306);
            properties.put("spring.datasource.url", "jdbc:mysql://" + rdbmsHost + ":" + rdbmsPort + "/container");
            properties.put("spring.datasource.username", "root");
            properties.put("spring.datasource.password", "password");
        }

        private void setRedisProperties(Map<String, String> properties) {
            String redisHost = DOCKER_COMPOSE.getServiceHost("redis", 6379);
            Integer redisPort = DOCKER_COMPOSE.getServicePort("redis", 6379);
            properties.put("spring.redis.host", redisHost);
            properties.put("spring.redis.port", redisPort.toString());
        }
    }
}

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” ์—ฌ๋Ÿฌ ์„œ๋น„์Šค์˜ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋™์‹œ์— ์‹คํ–‰ํ•  ๋•Œ Docker Compose๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ปจํ…Œ์ด๋„ˆ ๊ฐ„์˜ ์ดˆ๊ธฐํ™” ์‹œ๊ฐ„์ด ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ๋ชจ๋“  ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์‹œ์ž‘๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด 'Wait'๊ตฌ๋ฌธ์„ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.

private static final DockerComposeContainer<?> DOCKER_COMPOSE =
    new DockerComposeContainer<>(new File("src/test/resources/docker-compose.yml"))
        .withExposedService("mysql", 3306, Wait.forLogMessage(".*ready for connections.*", 1))
        .withExposedService("redis", 6379, Wait.forLogMessage(".*Ready to accept connections.*", 1));

Docker-compose.yml

version: "3.8"

services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: container
      MYSQL_ROOT_PASSWORD: password
    ports:
      - 3306 # Tip: ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” ํ˜ธ์ŠคํŠธ ํฌํŠธ ๋งคํ•‘(์˜ˆ: 3306:3306)์„ ์ƒ๋žตํ•˜๋ฉด ํฌํŠธ ์ถฉ๋Œ์„ ํ”ผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  redis:
    image: redis:6
    ports:
      - 6379 # Tip: ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” ํ˜ธ์ŠคํŠธ ํฌํŠธ ๋งคํ•‘(์˜ˆ: 6379:6379)์„ ์ƒ๋žตํ•˜๋ฉด ํฌํŠธ ์ถฉ๋Œ์„ ํ”ผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ ํ…Œ์ŠคํŠธ์šฉ docker-compose ์„ค์ •์—์„œ๋Š” ํ˜ธ์ŠคํŠธ์™€ ์ปจํ…Œ์ด๋„ˆ์˜ ํฌํŠธ ๋งคํ•‘์„ ์ƒ๋žตํ•˜์—ฌ ํฌํŠธ ์ถฉ๋Œ์˜ ์œ„ํ—˜์„ ์ตœ์†Œํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
 
 

 

ํ…Œ์ŠคํŠธ

์•„๋ž˜๋Š” IntefrationTest ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์•„ MySQL, Redis, Kafka ์—ฐ๋™์„ ํ…Œ์ŠคํŠธ ํ•œ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

๋”๋ณด๊ธฐ

 

class DatabaseTest extends IntegrationTest {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private EntityManager entityManager;

    @Test
    void saveAndRetrieveUserTest() {
        // given
        User user = new User(null, "dkswnkk");
        userRepository.save(user);

        // when
        entityManager.flush();
        entityManager.clear();
        var retrievedUser = userRepository.findById(1L);

        // then
        Assertions.assertEquals("dkswnkk", retrievedUser.get().getName());
    }
}

 

public class KafkaConsumerApplicationTests extends IntegrationTest {

    @Autowired
    private KafkaProducerService kafkaProducerService;

    // ์‹ค์ œ KafkaConsumerService ๋Œ€์‹  mock ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    // mock ๊ฐ์ฒด๋Š” ์‹ค์ œ ๋กœ์ง์„ ์‹คํ–‰ํ•˜์ง€ ์•Š์œผ๋ฉฐ, ํ˜ธ์ถœ ์—ฌ๋ถ€๋‚˜ ์ธ์ž ๊ฐ’ ๋“ฑ์„ ํ™•์ธํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
    @MockBean
    private KafkaConsumerService kafkaConsumerService;

    // ์นดํ”„์นด ๋ฉ”์‹œ์ง€ ์ „์†ก ๋ฐ ์ˆ˜์‹  ํ…Œ์ŠคํŠธ
    @Test
    public void kafkaSendAndConsumeTest() {
        String topic = "test-topic";
        String expectValue = "expect-value";

        kafkaProducerService.send(topic, expectValue);

        // ArgumentCaptor: mock ๊ฐ์ฒด์— ์ „๋‹ฌ๋œ ์ธ์ž๋ฅผ ์บก์ฒ˜ํ•ด ํ›„์— ๊ทธ ๊ฐ’์„ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
        var stringCaptor = ArgumentCaptor.forClass(String.class);

        // kafkaConsumerService.process ๋ฉ”์†Œ๋“œ๊ฐ€ ํŠน์ • ์‹œ๊ฐ„ ๋‚ด์— ํ•œ ๋ฒˆ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
        // ๋˜ํ•œ, ๊ทธ ๋•Œ ์–ด๋–ค ์ธ์ž๋กœ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ stringCaptor๋ฅผ ํ†ตํ•ด ์บก์ฒ˜ํ•ฉ๋‹ˆ๋‹ค.
        Mockito.verify(kafkaConsumerService, Mockito.timeout(5000).times(1))
                .process(stringCaptor.capture());

        // ์บก์ฒ˜๋œ ์ธ์ž์™€ ๊ธฐ๋Œ€๊ฐ’์ด ์ผ์น˜ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.
        Assertions.assertEquals(expectValue, stringCaptor.getValue());
    }
}

 

public class RedisTest extends IntegrationTest {
    @Autowired
    private RedisService redisService;

    @Test
    @DisplayName("Redis Get / Set ํ…Œ์ŠคํŠธ")
    public void redisGetSetTest() {
        // given
        String key = "name";
        String expectedValue = "dkswnkk";

        // when
        redisService.set(key, expectedValue);

        // then
        String actualValue = redisService.get(key);
        Assertions.assertEquals(expectedValue, actualValue);
    }
}

 

@Configuration
public class KafkaConsumerApplication {
    @Autowired
    private KafkaConsumerService kafkaConsumerService;

    // ์นดํ”„์นด ํ† ํ”ฝ ์„ค์ •
    @Bean
    public NewTopic topic() {
        return TopicBuilder.name("test-topic").build();
    }

    // ์นดํ”„์นด ๋ฆฌ์Šค๋„ˆ: ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›์œผ๋ฉด ์ฒ˜๋ฆฌ ์„œ๋น„์Šค๋ฅผ ํ˜ธ์ถœ
    @KafkaListener(id = "test-id", topics = "test-topic")
    public void listen(String message) {
        kafkaConsumerService.process(message);
    }
}

 

@Service
public class KafkaConsumerService {
    // ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ: ํ˜„์žฌ๋Š” ์ฝ˜์†”์— ์ถœ๋ ฅ
    public void process(String message) {
        System.out.println("processing ... " + message);
    }
}

 

@Service
public class KafkaProducerService {
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    // ์ฃผ์–ด์ง„ ํ† ํ”ฝ์œผ๋กœ ๋ฉ”์‹œ์ง€ ์ „์†ก
    public void send(String topic, String message) {
        kafkaTemplate.send(topic, message);
    }
}

 

@Service
public class RedisService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public String get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }
}

ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰ ๊ฒฐ๊ณผ

 
 

๊ฒฐ๋ก 

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ DB์™€ ๊ฐ™์€ ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์€ ๋ณต์žกํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. TestContainer๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์ด๋Ÿฌํ•œ ๋ณต์žก์„ฑ์„ ํฌ๊ฒŒ ์ค„์ด๊ณ , ๊ฒฉ๋ฆฌ๋œ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ, CI/CD ํŒŒ์ดํ”„๋ผ์ธ๊ณผ ๊ฐ™์ด ์ž๋™ํ™”๋œ ํ™˜๊ฒฝ์—์„œ๋Š” TestContainer๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋”์šฑ ์•ˆ์ •์ ์ธ ํ…Œ์ŠคํŠธ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

์ฐธ๊ณ 

 

ํ…Œ์ŠคํŠธ ๊ด€๋ จ ๊ฐ™์ด ๋ณด๋ฉด ์ข‹์€ ๊ธ€

๋Œ“๊ธ€