
ํ ์คํธ workflow ์์ฑ (feature → dev PR ์ ์คํ)
๊ธฐ์กด workflow๋ dev ๋ธ๋์น์ push ํ ๋๋ง๋ค ํ ์คํธ์ ๋ฐฐํฌ๊ฐ ํจ๊ป ๋์์ ํ๊ณ ์์๋๋ฐ,
ํ ์คํธ๊ฐ ํฌํจ๋๋ค ๋ณด๋ Github Actions ์คํ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๊ณ ,
BE๋ ํผ์ ๊ฐ๋ฐํ๋ค ๋ณด๋ PR ๊ณผ์ ์ ์๋ตํ์๋๋ฐ ์ด๋ฌ๋ฉด์ ๊ฒ์ฆ๋์ง ์์ ์ฝ๋๋ค์ด dev์ merge๋ ๊ฐ๋ฅ์ฑ์ด ์์๋ค.
์ด๋ฌํ ๋ฌธ์ ์ ๋ค์ ํด๊ฒฐํ๊ธฐ ์ํด์ workflow๋ฅผ ํ ์คํธ์ฉ/๋ฐฐํฌ์ฉ ๋ ๊ฐ์ง๋ก ๋ถ๋ฆฌํ๋ ์์ ์ ์งํํด๋ณด์๋ค. ๐ฅ
name: Test on Pull Request
on:
pull_request:
branches:
- dev
types: [opened, synchronize, reopened]
feature ๋ธ๋์น์์ ๊ฐ๋ฐ ํ dev ๋ธ๋์น์ Pull Reqeuset๋ฅผ ์ฌ๋ฆฌ๋ฉด workflow๊ฐ ์คํ๋๋ค.
type์ ์ง์ ํ ์ ์๋๋ฐ opened์ PR ์ต์ด ์์ฑ, synchronize๋ PR์ ์ ์ปค๋ฐ์ด ์ถ๊ฐ๋ ๋, reopened๋ ๋ซํ PR์ ๋ค์ ์ด์์ ๋ workfolw๊ฐ ์คํ๋๊ฒ ์ค์ ํ๋ค.
Testcontainers ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํ ํ ์คํธ ํ๊ฒฝ ๊ตฌ์ฑ
DB์ฐ๊ฒฐ์ด ํ์ํ ํ ์คํธ ์ฝ๋๋ค์ H2 database๋ฅผ ์ด์ฉํ์๋๋ฐ redis๋ ๋ฐ๋ก ํ ์คํธ ๊ตฌ์ฑ์ด ๋์ด์์ง ์์์ Testcontainers ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํด ํ ์คํธ ํ๊ฒฝ์ ๊ตฌ์ฑํ๋ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝํ๋ค.
Docker๋ฅผ ํตํด ๊ฒฉ๋ฆฌ๋ Redis์ PostgreSQL ์ปจํ ์ด๋๋ฅผ ์ผํ์ฉ์ผ๋ก ๋์ฐ๊ณ , ํ ์คํธ๊ฐ ๋๋๋ฉด ์๋์ผ๋ก ์ญ์ ๋๋๋ก ํ๋ค.
build.gradle ์์กด์ฑ ์ถ๊ฐ
testImplementation("org.springframework.boot:spring-boot-testcontainers:4.0.1")
testImplementation("com.redis.testcontainers:testcontainers-redis:1.6.4")
testImplementation("org.testcontainers:junit-jupiter:1.21.3")
testImplementation("org.testcontainers:postgresql:1.21.3")
application-test.yml ์์
spring:
datasource:
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
data:
redis:
์ด ๋ถ๋ถ์ ๋์ณ์ ๊ณ์ table ์์ฑ์ด ์ ๋ผ์ ๊ณ ์ํ๋๋ฐ ..
Testcontainers ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํ๋ฉด ์์์ ์ปจํ ์ด๋ ์ฃผ์๋ฅผ ์ฃผ์ ํด์ฃผ๊ธฐ ๋๋ฌธ์, ๊ผญ! ๊ฐ์ ๋น์์ค์ผ ํ๋ค.
ํ ์คํธ ํ๊ฒฝ ์ถ์ํ
@ServiceConnection ์ด๋ ธํ ์ด์ ์ ์ด์ฉํด์ Redis์ PostgreSQL ์ปจํ ์ด๋๋ฅผ ์ค์ ํ๋ ์ถ์ ํด๋์ค๋ฅผ ๋ง๋ค๊ณ , ํด๋น ํด๋์ค๋ฅผ ์์ ๋ฐ์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ค. ๊ธฐ์กด ํ ์คํธ ์ฝ๋๋ค์ด DescribeSpec์ ์์ ๋ฐ๊ณ ์์๊ธฐ ๋๋ฌธ์ ์์ฑ์์์ body๋ฅผ ๋ฐ์ DescribeSpec์๊ฒ ๋๊ฒจ์ฃผ๋ ๋ก์ง๋ ์ถ๊ฐํ๋ค.
@SpringBootTest
abstract class IntegrationTestSupport(body: DescribeSpec.() -> Unit = {}) : DescribeSpec(body) {
// Kotest์์ Spring์ ์ฐ๋ ค๋ฉด ์ด ํ์ฅ์ด ํ์
override fun extensions() = listOf(SpringExtension)
companion object {
// Redis
@Container
@ServiceConnection
val redisContainer = GenericContainer(DockerImageName.parse("redis:alpine"))
.withExposedPorts(6379)
// PostgreSQL
@Container
@ServiceConnection
val postgresContainer = PostgreSQLContainer<Nothing>(DockerImageName.parse("postgres:15-alpine")).apply {
withDatabaseName("testdb")
withUsername("test")
withPassword("test")
}
}
}

ํ์ง๋ง ๊ณ์ ๋๋ redis ์ฐ๊ฒฐ ์คํจ ๋ฌธ์ ... ๐คฏ
๊ณ์๋๋ ์ฝ์ง๊ณผ ๊ตฌ๊ธ๋ง, ์จ๊ฐ AI์ ์จ๋ฆ์ ํ๋ฉด์ ์ปจํ ์ด๋ ์ฐ๊ฒฐ ์ฑ๊ณต์ํจ ํ ์คํธ ์ถ์ ํด๋์ค๋ ์๋์ ๊ฐ๋ค.
@SpringBootTest
abstract class IntegrationTestSupport(
body: DescribeSpec.() -> Unit = {},
) : DescribeSpec(body) {
override fun extensions(): List<Extension> = listOf(SpringExtension)
companion object {
val redisContainer =
GenericContainer(DockerImageName.parse("redis:alpine"))
.withExposedPorts(6379)
.apply { start() }
@ServiceConnection
val postgresContainer =
PostgreSQLContainer<Nothing>(DockerImageName.parse("postgres:15-alpine"))
.apply {
withDatabaseName("testdb")
withUsername("test")
withPassword("test")
}.apply { start() }
@JvmStatic
@DynamicPropertySource
fun overrideProps(registry: DynamicPropertyRegistry) {
registry.add("spring.data.redis.host") { redisContainer.host }
registry.add("spring.data.redis.port") { redisContainer.getMappedPort(6379).toString() }
}
}
}
Redis ์ปจํ ์ด๋ ์ค์ ๋ Redis ์ ์ฉ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์์ด์ ์ ์ฉ์ ํด๋ดค๋๋ฐ, redis config์์ ๊ณ์ host, port๋ฅผ ์ฐพ์ง ๋ชป ํ๋ ์๋ฌ๊ฐ ์์ด์ ์ผ๋จ GenericContainer๋ก ๊ตฌํํ๋ค.
@ServiceConnection์ ํ์ ์ด ๋ช ํํ ๋๋ง ์ฌ์ฉํ ์ ์๊ธฐ ๋๋ฌธ์ redis ์ปจํ ์ด๋ ์ค์ ์์๋ ์ฌ์ฉํ์ง ์์๋ค.
ํน์ ํ ์คํธ ์ฝ๋ ์ ์ธ์ํค๊ธฐ
์ด๋ฏธ์ง ์ ๋ก๋ ํ ์คํธ๋ Google ํ ํฐ ๊ฒ์ฆ ํ ์คํธ๋ ๋ก์ปฌ ํ๊ฒฝ์์๋ง ๋์ํ๊ฒ ํ๊ณ ์ถ์ด์ @Tag("local-only") ์ด๋ ธํ ์ด์ ๋ฌ๋ฆฐ ํ ์คํธ ์ฝ๋๋ ๋์ํ์ง ์๊ฒ ์ค์ ์ ํด์คฌ๋ค. ํ์ง๋ง ํด๋น workflow ๋ก๊ทธ๋ฅผ ํ์ธํด๋ณด๋ ์ ์ธ์ํค๊ณ ์ถ์ ์ฝ๋๋ค์ด ๊ณ์ ํ ์คํธ๊ฐ ๋๊ณ ์์๋ค. ๐คฏ
- name: Run tests with Testcontainers
run: ./gradlew test -PexcludeTags="local-only" --no-daemon
env:
SPRING_PROFILES_ACTIVE: test
workflow ์์ฒด์๋ ๋ฌธ์ ์์๊ณ ๋ ๋ค์ ์์๋ ์ฝ์ง...
๊ฒฐ๊ตญ GPT๊ฐ ํด๋ต์ ๋ด์คฌ๋๋ฐ ๋ด๊ฐ ์ฌ์ฉํ๊ณ ์๋ @Tag ์ด๋ ธํ ์ด์ ์ JUnit ํ ์คํธ์๋ง ์ ์ฉ๋๊ณ ,
ํ์ฌ ๋ด ํ ์คํธ ์ฝ๋๋ค์ Kotest ์์ง์ผ๋ก ์คํ๋๊ธฐ ๋๋ฌธ์ @Tags ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํด์ผ ํ๋ค๊ณ ํ๋ค.
tasks.withType<Test> {
useJUnitPlatform()
systemProperty(
"kotest.tags.exclude",
project.findProperty("excludeTags") ?: "",
)
}
build.gradle๋ ์์ฒ๋ผ kotest ์ ์ฉ ํํฐ๋ง ์ค์ ์ ํด์คฌ๋๋ ์๋ํ๋๋ก ์ง์ ํด์ค ํ ์คํธ ์ฝ๋๋ค์ด ์ ์ธ๋๋ค.
ํ
์คํธ ์์ง, ํ
์คํธ ์ฝ๋ ์์ฑ, mock ๊ฐ์ฒด ๋ค๋ฃจ๋ ๋ฐฉ๋ฒ ์ด์ฌํ ๊ณต๋ถํด์ผ๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์๋ค ..๐๐ซ

์จ๊ฐ ์ฝ์ง ๋์ ๋๋์ด ํ ์คํธ ํต๊ณผ์์ผฐ๋ค.
๋ค์์ dev ๋ฐฐํฌํ๋ workflow ์์ฑํ ์์ ์ ๋๋ค.