๐ฏ Kotest BDD vs TDD ์์ ์ ๋ณต: ์ฝํ๋ฆฐ ์คํ๋ง๋ถํธ ํ ์คํ ๋ง์คํฐ ๊ฐ์ด๋
๊ด๋ฆฌ์
1์ผ ์
๐ฏ Kotest BDD vs TDD ์์ ์ ๋ณต: ์ฝํ๋ฆฐ ์คํ๋ง๋ถํธ ํ ์คํ ๋ง์คํฐ ๊ฐ์ด๋
๐ฏ ํ ์ค ์์ฝ
BehaviorSpec vs FunSpec vs JUnit? ์ฝํ๋ฆฐ ์คํ๋ง๋ถํธ์์ BDD์ TDD ์คํ์ผ ํ ์คํ ์ ์๋ฒฝํ๊ฒ ๋น๊ต๋ถ์ํ๊ณ ์ค์ ์ ์ฉ๋ฒ๊น์ง ํ ๋ฒ์ ์ ๋ฆฌํ์ต๋๋ค!
๐ค ํ ์คํ ์คํ์ผ ์ ํ์ ๊ณ ๋ฏผ
์ฝํ๋ฆฐ ์คํ๋ง๋ถํธ ํ๋ก์ ํธ๋ฅผ ์์ํ ๋ ์ด๋ฐ ๊ณ ๋ฏผ ํด๋ณด์ จ๋์?
- "Kotest BehaviorSpec์ด BDD๋ผ๋๋ฐ, TDD๋ ๋ญ๋ก ํด์ผ ํ์ง?"
- "JUnit 5 vs Kotest, ์ด๋ค ๊ฑธ ์ ํํด์ผ ํ ๊น?"
- "Given-When-Then vs Test ํจ์, ์ธ์ ๋ญ ์จ์ผ ํ ๊น?"
์ค๋ ์ด ๋ชจ๋ ๊ณ ๋ฏผ์ ์์ ํ ํด๊ฒฐํด๋๋ฆฌ๊ฒ ์ต๋๋ค! ๐ฅ
๐ BDD vs TDD: ๊ฐ๋ ๋ถํฐ ์ ๋ฆฌํ์
๐ฏ TDD (Test-Driven Development)
"์คํจํ๋ ํ ์คํธ๋ฅผ ๋จผ์ ์์ฑํ๊ณ , ์ต์ํ์ ์ฝ๋๋ก ํต๊ณผ์ํจ ํ ๋ฆฌํฉํ ๋ง"
TDD 3๋จ๊ณ ์ฌ์ดํด:
- Red: ์คํจํ๋ ํ ์คํธ ์์ฑ
- Green: ํ ์คํธ๋ฅผ ํต๊ณผํ๋ ์ต์ ์ฝ๋ ์์ฑ
- Refactor: ์ฝ๋ ๊ฐ์
๐ฏ BDD (Behavior-Driven Development)
"๋น์ฆ๋์ค ์๊ตฌ์ฌํญ์ ์์ฐ์ด์ ๊ฐ๊น์ด ํํ๋ก ํ ์คํธ ์๋๋ฆฌ์ค ์์ฑ"
BDD Given-When-Then ํจํด:
- Given: ์ฃผ์ด์ง ์ํฉ (์ ์ ์กฐ๊ฑด)
- When: ํน์ ๋์์ ์คํํ์ ๋
- Then: ์์๋๋ ๊ฒฐ๊ณผ
๐ ์ฝํ๋ฆฐ์์์ BDD vs TDD ๊ตฌํ ๋ฐฉ์
1๏ธโฃ BDD ์คํ์ผ: Kotest BehaviorSpec
BDD๋ ๋น์ฆ๋์ค ์๊ตฌ์ฌํญ์ ์ค์ฌ์ผ๋ก ํ ํ๋ ๊ฒ์ฆ์ ๋๋ค:
// BDD ์คํ์ผ: BehaviorSpec (์์ฐ์ด์ ๊ฐ๊น์ด ํํ)
class UserServiceBehaviorTest : BehaviorSpec({
Given("์ ๊ท ์ฌ์ฉ์ ์ ๋ณด๊ฐ ์ฃผ์ด์ก์ ๋") {
val userService = UserService()
val validUserRequest = CreateUserRequest(
username = "kimkotlin",
email = "kim@kotlin.com",
password = "password123"
)
When("์ฌ์ฉ์ ์์ฑ์ ์์ฒญํ๋ฉด") {
val createdUser = userService.createUser(validUserRequest)
Then("์ฌ์ฉ์๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์์ฑ๋๋ค") {
createdUser.id shouldBeGreaterThan 0
createdUser.username shouldBe "kimkotlin"
createdUser.email shouldBe "kim@kotlin.com"
}
Then("ํจ์ค์๋๋ ์ํธํ๋์ด ์ ์ฅ๋๋ค") {
createdUser.password shouldNotBe "password123"
createdUser.password shouldStartWith "$2a$"
}
}
When("์ค๋ณต๋ ์ด๋ฉ์ผ๋ก ์ฌ์ฉ์ ์์ฑ์ ์๋ํ๋ฉด") {
userService.createUser(validUserRequest) // ์ฒซ ๋ฒ์งธ ์์ฑ
Then("DuplicateEmailException์ด ๋ฐ์ํ๋ค") {
shouldThrow<DuplicateEmailException> {
userService.createUser(validUserRequest) // ์ค๋ณต ์๋
}
}
}
}
Given("๊ธฐ์กด ์ฌ์ฉ์๊ฐ ์กด์ฌํ ๋") {
val userService = UserService()
val existingUser = userService.createUser(
CreateUserRequest("existing", "existing@test.com", "pass")
)
When("์ ํจํ ID๋ก ์ฌ์ฉ์๋ฅผ ์กฐํํ๋ฉด") {
val foundUser = userService.findById(existingUser.id)
Then("ํด๋น ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐํํ๋ค") {
foundUser shouldNotBe null
foundUser!!.username shouldBe "existing"
}
}
When("์กด์ฌํ์ง ์๋ ID๋ก ์กฐํํ๋ฉด") {
Then("UserNotFoundException์ด ๋ฐ์ํ๋ค") {
shouldThrow<UserNotFoundException> {
userService.findById(999L)
}
}
}
}
})
2๏ธโฃ TDD ์คํ์ผ: Kotest FunSpec / JUnit
TDD๋ ๊ธฐ๋ฅ ๋จ์์ ๋น ๋ฅธ ๊ฒ์ฆ์ ์ ํฉํฉ๋๋ค:
A. Kotest FunSpec (์ฝํ๋ฆฐ ๋ค์ดํฐ๋ธ)
class UserServiceFunTest : FunSpec({
// Red โ Green โ Refactor ์ฌ์ดํด์ ์ ํฉ
test("์ฌ์ฉ์ ์์ฑ ์ฑ๊ณต ํ
์คํธ") {
// Arrange
val userService = UserService()
val request = CreateUserRequest("test", "test@example.com", "password")
// Act
val result = userService.createUser(request)
// Assert
result.id shouldBeGreaterThan 0
result.username shouldBe "test"
result.email shouldBe "test@example.com"
}
test("์ค๋ณต ์ด๋ฉ์ผ ๊ฒ์ฆ ํ
์คํธ") {
val userService = UserService()
val request = CreateUserRequest("test", "test@example.com", "password")
// ์ฒซ ๋ฒ์งธ ์ฌ์ฉ์ ์์ฑ
userService.createUser(request)
// ์ค๋ณต ์๋ ์ ์์ธ ๋ฐ์ ๊ฒ์ฆ
shouldThrow<DuplicateEmailException> {
userService.createUser(request)
}
}
test("์ฌ์ฉ์ ์กฐํ ํ
์คํธ") {
val userService = UserService()
val created = userService.createUser(
CreateUserRequest("lookup", "lookup@test.com", "pass")
)
val found = userService.findById(created.id)
found shouldNotBe null
found!!.username shouldBe "lookup"
}
test("์กด์ฌํ์ง ์๋ ์ฌ์ฉ์ ์กฐํ ์์ธ ํ
์คํธ") {
val userService = UserService()
shouldThrow<UserNotFoundException> {
userService.findById(999L)
}
}
})
B. JUnit 5 (์ ํต์ ๋ฐฉ์)
@ExtendWith(SpringExtension::class)
@SpringBootTest
class UserServiceJUnitTest {
@Autowired
private lateinit var userService: UserService
@Test
@DisplayName("์ฌ์ฉ์ ์์ฑ ์ฑ๊ณต")
fun `should create user successfully`() {
// Arrange
val request = CreateUserRequest("junit", "junit@test.com", "password")
// Act
val result = userService.createUser(request)
// Assert
assertThat(result.id).isGreaterThan(0)
assertThat(result.username).isEqualTo("junit")
assertThat(result.email).isEqualTo("junit@test.com")
}
@Test
@DisplayName("์ค๋ณต ์ด๋ฉ์ผ ์์ธ")
fun `should throw exception for duplicate email`() {
val request = CreateUserRequest("duplicate", "dup@test.com", "pass")
userService.createUser(request)
assertThrows<DuplicateEmailException> {
userService.createUser(request)
}
}
}
โ๏ธ ์คํ์ผ๋ณ ์ฌํ ๋น๊ต ๋ถ์
๐ ํน์ง ๋น๊ตํ
๊ตฌ๋ถ | BDD (BehaviorSpec) | TDD (FunSpec) | TDD (JUnit 5) |
---|---|---|---|
๊ฐ๋ ์ฑ | โญโญโญโญโญ (์์ฐ์ด) | โญโญโญโญ | โญโญโญ |
์์ฑ ์๋ | โญโญโญ | โญโญโญโญโญ | โญโญโญโญ |
์ ์ง๋ณด์ | โญโญโญโญ | โญโญโญโญ | โญโญโญ |
ํ์ ์นํ์ฑ | โญโญโญโญโญ | โญโญโญ | โญโญ |
๋ฌ๋ ์ปค๋ธ | โญโญโญ | โญโญโญโญโญ | โญโญโญโญ |
๐ฏ ์ธ์ ์ด๋ค ์คํ์ผ์ ์ฌ์ฉํด์ผ ํ ๊น?
โ BDD (BehaviorSpec) ์ฌ์ฉ ์๊ธฐ:
- ๋น์ฆ๋์ค ๋ก์ง์ด ๋ณต์กํ ์๋น์ค
- PM, QA์ ํ์ ์ด ์ค์ํ ํ๋ก์ ํธ
- ์ฌ์ฉ์ ์๋๋ฆฌ์ค ๊ธฐ๋ฐ ํ ์คํธ
- ์ธ์ ํ ์คํธ (Acceptance Test)
// ์์: ์ฃผ๋ฌธ ์ฒ๋ฆฌ ์์คํ
Given("์ฅ๋ฐ๊ตฌ๋์ ์ํ์ด ๋ด๊ฒจ์๊ณ ๊ฒฐ์ ์๋จ์ด ๋ฑ๋ก๋ ์ํ์์") {
When("์ฃผ๋ฌธ์ ์์ฒญํ๋ฉด") {
Then("์ฃผ๋ฌธ์ด ์์ฑ๋๊ณ ") {
And("์ฌ๊ณ ๊ฐ ์ฐจ๊ฐ๋๊ณ ") {
And("๊ฒฐ์ ๊ฐ ์งํ๋์ด์ผ ํ๋ค") {
}
}
โ TDD (FunSpec/JUnit) ์ฌ์ฉ ์๊ธฐ:
- ๋จ์ ํ ์คํธ (Unit Test)
- ๋น ๋ฅธ ํผ๋๋ฐฑ์ด ํ์ํ ๊ฐ๋ฐ
- ์๊ณ ๋ฆฌ์ฆ, ์ ํธ๋ฆฌํฐ ํจ์ ํ ์คํธ
- Red-Green-Refactor ์ฌ์ดํด
test("์ํธํ ํจ์ ๊ฒ์ฆ") {
val plainText = "password123"
val encrypted = encrypt(plainText)
encrypted shouldNotBe plainText
decrypt(encrypted) shouldBe plainText
}
๐ Spring Boot ํตํฉ ์ค์ ๊ฐ์ด๋
1๏ธโฃ ์์กด์ฑ ์ค์
dependencies {
// Kotest
testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
testImplementation("io.kotest:kotest-assertions-core:5.8.0")
testImplementation("io.kotest:kotest-extensions-spring:1.1.3")
// Spring Boot Test
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.mockito", module = "mockito-core")
}
// MockK (Mockito ๋์ )
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("com.ninja-squad:springmockk:4.0.2")
}
tasks.withType<Test> {
useJUnitPlatform()
}
2๏ธโฃ ์ค์ Spring Boot ์ปจํธ๋กค๋ฌ ํ ์คํธ
BDD ์คํ์ผ: API ์๋๋ฆฌ์ค ํ ์คํธ
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserControllerBehaviorTest : BehaviorSpec() {
@Autowired
private lateinit var testRestTemplate: TestRestTemplate
init {
Given("์ฌ์ฉ์ API ์๋ํฌ์ธํธ๊ฐ ์ค๋น๋ ์ํ์์") {
When("์ ํจํ ์ฌ์ฉ์ ์์ฑ ์์ฒญ์ ๋ณด๋ด๋ฉด") {
val request = CreateUserRequest(
username = "apitester",
email = "api@test.com",
password = "secure123"
)
val response = testRestTemplate.postForEntity(
"/api/users",
request,
UserResponse::class.java
)
Then("201 Created ์๋ต์ ๋ฐ๋๋ค") {
response.statusCode shouldBe HttpStatus.CREATED
}
Then("์์ฑ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐํํ๋ค") {
val userResponse = response.body!!
userResponse.username shouldBe "apitester"
userResponse.email shouldBe "api@test.com"
userResponse.id shouldBeGreaterThan 0
}
}
When("์๋ชป๋ ์ด๋ฉ์ผ ํ์์ผ๋ก ์์ฒญํ๋ฉด") {
val invalidRequest = CreateUserRequest(
username = "invalid",
email = "not-an-email",
password = "pass"
)
val response = testRestTemplate.postForEntity(
"/api/users",
invalidRequest,
ErrorResponse::class.java
)
Then("400 Bad Request ์๋ต์ ๋ฐ๋๋ค") {
response.statusCode shouldBe HttpStatus.BAD_REQUEST
}
Then("๊ฒ์ฆ ์ค๋ฅ ๋ฉ์์ง๋ฅผ ํฌํจํ๋ค") {
response.body!!.message should include("์ด๋ฉ์ผ ํ์")
}
}
}
}
}
TDD ์คํ์ผ: ์๋น์ค ๋ ์ด์ด ๋จ์ ํ ์คํธ
@ExtendWith(SpringExtension::class)
class UserServiceUnitTest : FunSpec() {
private val userRepository = mockk<UserRepository>()
private val passwordEncoder = mockk<PasswordEncoder>()
private val userService = UserService(userRepository, passwordEncoder)
init {
test("์ฌ์ฉ์ ์์ฑ ์ ํจ์ค์๋ ์ํธํ ํ์ธ") {
// Given
val plainPassword = "password123"
val encodedPassword = "$2a$10$encoded"
val request = CreateUserRequest("user", "user@test.com", plainPassword)
every { userRepository.existsByEmail(any()) } returns false
every { passwordEncoder.encode(plainPassword) } returns encodedPassword
every { userRepository.save(any()) } returns User(
id = 1L,
username = "user",
email = "user@test.com",
password = encodedPassword
)
// When
val result = userService.createUser(request)
// Then
result.password shouldBe encodedPassword
verify { passwordEncoder.encode(plainPassword) }
verify { userRepository.save(any()) }
}
test("์ด๋ฉ์ผ ์ค๋ณต ์ ์์ธ ๋ฐ์") {
// Given
every { userRepository.existsByEmail("dup@test.com") } returns true
// When & Then
shouldThrow<DuplicateEmailException> {
userService.createUser(
CreateUserRequest("dup", "dup@test.com", "pass")
)
}
verify(exactly = 0) { userRepository.save(any()) }
}
}
}
3๏ธโฃ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ ํ ์คํธ
BDD ์คํ์ผ: Repository ํ๋ ๊ฒ์ฆ
@DataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserRepositoryBehaviorTest : BehaviorSpec() {
@Autowired
private lateinit var userRepository: UserRepository
@Autowired
private lateinit var testEntityManager: TestEntityManager
init {
Given("์ฌ์ฉ์ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋์ด ์์ ๋") {
val user1 = User(username = "active1", email = "active1@test.com", status = "ACTIVE")
val user2 = User(username = "inactive1", email = "inactive1@test.com", status = "INACTIVE")
val user3 = User(username = "active2", email = "active2@test.com", status = "ACTIVE")
testEntityManager.persistAndFlush(user1)
testEntityManager.persistAndFlush(user2)
testEntityManager.persistAndFlush(user3)
When("ํ์ฑ ์ฌ์ฉ์๋ง ์กฐํํ๋ฉด") {
val activeUsers = userRepository.findByStatus("ACTIVE")
Then("ํ์ฑ ์ํ ์ฌ์ฉ์ 2๋ช
์ด ์กฐํ๋๋ค") {
activeUsers.size shouldBe 2
activeUsers.all { it.status == "ACTIVE" } shouldBe true
}
}
When("์ด๋ฉ์ผ๋ก ์ฌ์ฉ์๋ฅผ ๊ฒ์ํ๋ฉด") {
val foundUser = userRepository.findByEmail("active1@test.com")
Then("ํด๋น ์ฌ์ฉ์๊ฐ ์ ํํ ์กฐํ๋๋ค") {
foundUser shouldNotBe null
foundUser!!.username shouldBe "active1"
}
}
}
}
}
๐ช ๊ณ ๊ธ ํ ์คํ ํจํด
1๏ธโฃ ์ค์ฒฉ ์ปจํ ์คํธ๋ก ์๋๋ฆฌ์ค ๊ตฌ์ฑ
class OrderServiceAdvancedTest : BehaviorSpec({
Given("์ฃผ๋ฌธ ์์คํ
์ด ์ค๋น๋์ด ์๊ณ ") {
val orderService = OrderService()
context("์ถฉ๋ถํ ์ฌ๊ณ ๊ฐ ์๋ ์ํ์ ๋ํด") {
val productId = 1L
val availableStock = 100
When("10๊ฐ ์ฃผ๋ฌธ์ ์์ฒญํ๋ฉด") {
val order = orderService.createOrder(productId, quantity = 10)
Then("์ฃผ๋ฌธ์ด ์ฑ๊ณตํ๋ค") {
order.status shouldBe OrderStatus.CONFIRMED
order.quantity shouldBe 10
}
Then("์ฌ๊ณ ๊ฐ ์ฐจ๊ฐ๋๋ค") {
val remainingStock = stockService.getStock(productId)
remainingStock shouldBe (availableStock - 10)
}
}
When("์ฌ๊ณ ๋ณด๋ค ๋ง์ ์๋์ ์ฃผ๋ฌธํ๋ฉด") {
Then("์ฌ๊ณ ๋ถ์กฑ ์์ธ๊ฐ ๋ฐ์ํ๋ค") {
shouldThrow<InsufficientStockException> {
orderService.createOrder(productId, quantity = 200)
}
}
}
}
context("์ฌ๊ณ ๊ฐ ๋ถ์กฑํ ์ํ์ ๋ํด") {
val lowStockProductId = 2L
When("์ฃผ๋ฌธ์ ์๋ํ๋ฉด") {
Then("์ฆ์ ์ฌ๊ณ ๋ถ์กฑ ์์ธ๊ฐ ๋ฐ์ํ๋ค") {
shouldThrow<InsufficientStockException> {
orderService.createOrder(lowStockProductId, quantity = 1)
}
}
}
}
}
})
2๏ธโฃ ํ๋กํผํฐ ๊ธฐ๋ฐ ํ ์คํ ํ์ฉ
class ValidationTest : FunSpec({
test("์ด๋ฉ์ผ ๊ฒ์ฆ ๋ก์ง์ ๊ฒฌ๊ณ ์ฑ ํ
์คํธ") {
checkAll<String> { randomString ->
val isValid = EmailValidator.isValid(randomString)
if (isValid) {
// ์ ํจํ ์ด๋ฉ์ผ์ด๋ผ๋ฉด @ ๊ธฐํธ๊ฐ ํฌํจ๋์ด์ผ ํจ
randomString should contain("@")
randomString shouldNot startWith("@")
randomString shouldNot endWith("@")
}
}
}
test("ํจ์ค์๋ ์ํธํ ์ผ๊ด์ฑ ํ
์คํธ") {
checkAll(Gen.string().filter { it.isNotBlank() }) { password ->
val encrypted1 = passwordEncoder.encode(password)
val encrypted2 = passwordEncoder.encode(password)
// ๋์ผ ๋น๋ฐ๋ฒํธ๋ผ๋ ๋งค๋ฒ ๋ค๋ฅธ ํด์๊ฐ ์์ฑ
encrypted1 shouldNotBe encrypted2
// ํ์ง๋ง ๊ฒ์ฆ์ ๋ ๋ค ์ฑ๊ณต
passwordEncoder.matches(password, encrypted1) shouldBe true
passwordEncoder.matches(password, encrypted2) shouldBe true
}
}
})
โก ์ค์ ํ๊ณผ ๋ฒ ์คํธ ํ๋ํฐ์ค
๐ฏ ํ ์คํธ ์กฐ์งํ ์ ๋ต
1. ํจํค์ง ๊ตฌ์กฐ
src/test/kotlin/
โโโ behavior/ # BDD ํ
์คํธ
โ โโโ user/
โ โโโ order/
โ โโโ payment/
โโโ unit/ # TDD ๋จ์ ํ
์คํธ
โ โโโ service/
โ โโโ util/
โ โโโ validator/
โโโ integration/ # ํตํฉ ํ
์คํธ
โโโ api/
โโโ repository/
โโโ external/
2. ๋ค์ด๋ฐ ์ปจ๋ฒค์
// BDD: ์์ฐ์ด์ ๊ฐ๊น์ด ์ค๋ช
Given("์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ๋์ด ์๊ณ ๊ถํ์ด ์์ ๋")
When("๊ฒ์๊ธ ์ญ์ ๋ฅผ ์์ฒญํ๋ฉด")
Then("๊ฒ์๊ธ์ด ์ฑ๊ณต์ ์ผ๋ก ์ญ์ ๋๋ค")
// TDD: ๊ธฐ๋ฅ ์ค์ฌ์ ๊ฐ๊ฒฐํ ์ค๋ช
test("์ ํจํ ํ ํฐ์ผ๋ก ์ธ์ฆ ์ฑ๊ณต")
test("๋ง๋ฃ๋ ํ ํฐ์ผ๋ก ์ธ์ฆ ์คํจ")
test("์๋ชป๋ ํ์ ํ ํฐ ๊ฒ์ฆ ์์ธ")
๐ฅ ์ฑ๋ฅ ์ต์ ํ ํ
1. ํ ์คํธ ๋ณ๋ ฌ ์คํ
// build.gradle.kts
tasks.test {
systemProperty("kotest.framework.parallelism", "4")
maxParallelForks = 4
}
2. ํ ์คํธ ๋ฐ์ดํฐ ์ต์ ํ
// ํ
์คํธ ์ ์ฉ ํ๋กํ ์ค์
@ActiveProfiles("test")
@TestPropertySource(
properties = [
"spring.jpa.hibernate.ddl-auto=create-drop",
"spring.datasource.url=jdbc:h2:mem:testdb"
]
)
๐ข ์ค์ ๋์ ์ฌ๋ก์ ๋ง์ด๊ทธ๋ ์ด์
๐ ๊ธฐ์ ์ฌ๋ก ๋ถ์
ํ์ฌ | ์ด์ ์คํ | ํ์ฌ ์คํ | ๋ง์ด๊ทธ๋ ์ด์ ์ด์ |
---|---|---|---|
์นด์นด์ค๋ฑ ํฌ | JUnit + Mockito | Kotest + MockK | ์ฝํ๋ฆฐ ์นํ์ฑ, ๊ฐ๋ ์ฑ ํฅ์ |
ํ ์ค | JUnit ํผํฉ | BDD/TDD ๋ถ๋ฆฌ | ๋น์ฆ๋์ค ๋ก์ง ๊ฒ์ฆ ๊ฐํ |
๋ฐฐ๋ฌ์๋ฏผ์กฑ | JUnit ์ค์ฌ | Kotest ์ ์ง ๋์ | ๊ฐ๋ฐ ์์ฐ์ฑ ํฅ์ |
๐ ๏ธ ๋จ๊ณ๋ณ ๋ง์ด๊ทธ๋ ์ด์ ๊ฐ์ด๋
Phase 1: ์ ๊ท ์ฝ๋๋ถํฐ ์์
// ๊ธฐ์กด JUnit ํ
์คํธ๋ ์ ์งํ๋ฉด์
@Test
fun existingJUnitTest() {
// ๊ธฐ์กด ํ
์คํธ ๊ทธ๋๋ก ์ ์ง
}
// ์๋ก์ด ๊ธฐ๋ฅ์ Kotest๋ก
class NewFeatureTest : FunSpec({
test("์๋ก์ด ๊ธฐ๋ฅ ํ
์คํธ") {
// Kotest ์คํ์ผ๋ก ์์ฑ
}
})
Phase 2: ์ค์ ๋น์ฆ๋์ค ๋ก์ง์ BDD๋ก
class PaymentProcessBehaviorTest : BehaviorSpec({
Given("๊ฒฐ์ ๊ฐ๋ฅํ ์ํ์ ์ฃผ๋ฌธ์ด ์์ ๋") {
When("์ ์ฉ์นด๋๋ก ๊ฒฐ์ ๋ฅผ ์๋ํ๋ฉด") {
Then("๊ฒฐ์ ๊ฐ ์ฑ๊ณตํ๊ณ ์ฃผ๋ฌธ ์ํ๊ฐ ๋ณ๊ฒฝ๋๋ค") {
// ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง์ BDD๋ก ๋ช
ํํ๊ฒ ๊ฒ์ฆ
}
}
}
})
Phase 3: ์ ํธ๋ฆฌํฐ์ ๋จ์ ํ ์คํธ๋ฅผ TDD๋ก
class DateUtilTest : FunSpec({
test("๋ ์ง ํฌ๋งคํ
์ ํ์ฑ ๊ฒ์ฆ") {
// ๋น ๋ฅธ ํผ๋๋ฐฑ์ด ํ์ํ ์ ํธ๋ฆฌํฐ ํจ์ ํ
์คํธ
}
})
๐ ํ ์คํธ ํ์ง ์ธก์ ๊ณผ ๊ฐ์
๐ฏ ์ปค๋ฒ๋ฆฌ์ง vs ํ์ง
// โ ๋์ ์: ์ปค๋ฒ๋ฆฌ์ง๋ง ๋์ ํ
์คํธ
test("๋ชจ๋ getter ํธ์ถ") {
val user = User("test", "test@test.com")
user.username // ๋จ์ ํธ์ถ
user.email // ์๋ฏธ ์๋ ํ
์คํธ
}
// โ
์ข์ ์: ์๋ฏธ ์๋ ํ๋ ๊ฒ์ฆ
Given("์ฌ์ฉ์ ์ ๋ณด๊ฐ ์ฃผ์ด์ก์ ๋") {
When("ํ๋กํ ์
๋ฐ์ดํธ๋ฅผ ์์ฒญํ๋ฉด") {
Then("๋ณ๊ฒฝ๋ ์ ๋ณด๊ฐ ์ ํํ ๋ฐ์๋๋ค") {
// ์ค์ ๋น์ฆ๋์ค ๊ฐ์น๊ฐ ์๋ ๊ฒ์ฆ
}
}
}
๐ ํ ์คํธ ๋ฉํธ๋ฆญ ์ถ์
// ํ
์คํธ ์คํ ์๊ฐ ๋ชจ๋ํฐ๋ง
class PerformanceTest : FunSpec({
test("API ์๋ต์๊ฐ ๊ฒ์ฆ").config(timeout = 3.seconds) {
val startTime = System.currentTimeMillis()
val response = apiClient.getUserData()
val endTime = System.currentTimeMillis()
(endTime - startTime) shouldBeLessThan 1000
}
})
๐ญ ๋ง๋ฌด๋ฆฌ: ์ต์ ์ ํ ์คํ ์ ๋ต ์ ํ
BDD์ TDD๋ ๊ฒฝ์ ๊ด๊ณ๊ฐ ์๋ ์ํธ ๋ณด์ ๊ด๊ณ์ ๋๋ค.
๐ฏ ๊ถ์ฅ ์กฐํฉ ์ ๋ต:
๐ ํ๋ก์ ํธ ์ด๊ธฐ:
- ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง: BDD (BehaviorSpec)
- ์ ํธ๋ฆฌํฐ/ํฌํผ ํจ์: TDD (FunSpec)
- ๋จ์ ํ ์คํธ: TDD (FunSpec ๋๋ JUnit)
๐๏ธ ํ๋ก์ ํธ ์ฑ์ฅ๊ธฐ:
- ์ฌ์ฉ์ ์๋๋ฆฌ์ค: BDD๋ก ์ ํ
- ์ฑ๋ฅ ํ ์คํธ: TDD ์ ์ง
- ํตํฉ ํ ์คํธ: BDD/TDD ํผํฉ
๐ ๋๊ท๋ชจ ์ด์:
- ์ธ์ ํ ์คํธ: BDD ์ค์ฌ
- ํ๊ท ํ ์คํธ: TDD ์ค์ฌ
- ๋ชจ๋ํฐ๋ง ํ ์คํธ: TDD ์ค์ฌ
๊ฒฐ๊ตญ ํ์ ์ํฉ๊ณผ ํ๋ก์ ํธ ํน์ฑ์ ๋ง๊ฒ ์ ์ฐํ๊ฒ ์กฐํฉํ๋ ๊ฒ์ด ์ ๋ต์ ๋๋ค!
์ฌ๋ฌ๋ถ์ ํ๋ก์ ํธ์์๋ ์ด๋ค ํ ์คํ ์ ๋ต์ ์ ํํ์ค ๊ฑด๊ฐ์? ๋๊ธ๋ก ๊ฒฝํ์ ๊ณต์ ํด์ฃผ์ธ์! ๐ฏ
์ด ๊ธ์ด ๋์์ด ๋์ จ๋ค๋ฉด ์ข์์์ ๊ณต์ ๋ถํ๋๋ฆฝ๋๋ค! โค๏ธ
๐ ์ฐธ๊ณ ์๋ฃ
๋๊ธ 0๊ฐ
์์ง ๋๊ธ์ด ์์ต๋๋ค
์ฒซ ๋ฒ์งธ ๋๊ธ์ ์์ฑํด๋ณด์ธ์!