테스트 주도 개발 (TDD)

 💡 TDD를 활용한 개발의 장점을 알고, 리팩토링하는 과정을 학습하였습니다.

TDD

  • 테스트 주도 개발로 코드를 작성하기 전에 테스트를 먼저 작성하는 개발 방법론입니다.
  • 아래와 같은 작업을, 매우 짧은 사이클로 반복한다는 특징이 있습니다.
    • [빨강] 실패하는 작은 테스트 작성
    • [초록] 최대한 빠른 테스트 통과
    • [파랑] 리팩터링 과정을 통해서 중복 & 코드 개선

  • TDD의 목적은 코드가 테스트에 지정한 요구 사항을 충족하는지 확인하고, 코드를 리팩토링하기 쉽게 수정하는 것입니다.

TDD 원칙

  • 실패하는 단위 테스트를 작성할 때까지 구현 코드를 작성하지 않습니다.
  • 컴파일은 실패하지 않지만, 실행이 실패하는 정도로만 코드를 작성합니다.
  • 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성합니다.

Kotlin에서의 TDD

  • Kotlin은 Java를 포함한 다른 언어에 비해 매우 간결한 언어로, 적은 코드를 통해 기능을 구현할 수 있습니다.
  • 코드 수가 적을수록 테스트를 작성하는 시간이 감소하므로 Kotlin은 TDD에 적합한 언어라고도 할 수 있습니다.

TDD 구성하기

  • BankAccount(은행 계좌)라는 도메인 클래스를 구성한다고 할 때, TDD를 구현해보겠습니다.

테스트 작성

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class BankAccountTest {
    private lateinit var account: BankAccount

    @BeforeEach
    fun setUp() {
        account = BankAccount()
    }

    @Test
    fun testInitialBalance() {
        assertEquals(0, account.getBalance())
    }

    @Test
    fun testDeposit() {
        account.deposit(100)
        assertEquals(100, account.getBalance())
    }
}
  • 우선 테스트를 작성하였습니다.
  • account에는 아래와 같은 두가지 메서드를 가지고 있습니다.
    • 입금을 진행하는 deposit()
    • 잔액 확인을 진행하는 getBalance()

도메인 클래스 작성

class BankAccount {
    private var balance: Int = 0

    fun getBalance(): Int {
        return balance
    }

    fun deposit(amount: Int) {
        balance += amount
    }
}
  • 이 후 원하는 도메인 클래스를 생성하였습니다.
  • 테스트 코드 작성 시 기대하는 값으로 반환하도록 메서드를 구현하였습니다.

리팩토링 진행

  • 도메인 클래스를 작성하였다면, 리팩토링을 진행할 수 있습니다.
  • 기능이 적은 클래스이므로, 변수 이름 개선  초기화 로직을 분리하는 간단한 리팩토링을 진행하였습니다.
  • 또한 예외 처리를 추가하여 유효하지 않은 입력을 방지하고자 하였습니다.
class BankAccount {
    private var currentBalance: Int = 0

    init {
        initialize()
    }

    private fun initialize() {
        currentBalance = 0
    }

    fun getBalance(): Int = currentBalance

    fun deposit(amount: Int) {
		    require(amount > 0)
        currentBalance += amount
    }
}

반복하기

  • 테스트 코드 작성 → 도메인 로직 구현 → 리팩토링 단계를 거쳤으므로, 다시 테스트 코드를 수정해야 합니다.
  • deposit() 메서드를 호출할 때 유효성을 검사하므로, 실패하는 테스트를 작성하여 유효성 검증이 제대로 이루어지는지 확인할 수 있습니다.
@Test
fun testDepositNegativeAmount() {
		val exception = assertThrows<IllegalArgumentException> {
				account.deposit(-100)
    }
    assertEquals("Deposit amount must be positive", exception.message)
}
  • 이와 같이 리팩토링 과정을 통해 점진적으로 기능을 개선하면서 도메인을 TDD로 구현할 수 있습니다.

결론

  • TDD를 활용한다면 좋은 설계를 할 수 있는 밑바탕이 되며, CI/CD에 적용하여 테스트를 자동화할 수 있습니다.
  • 다양한 케이스를 먼저 생각하고 개발하면 코드 작성 시 간결하고 좋은 코드를 작성할 수 있습니다.
  • 축적된 테스트 코드는 높아진 테스트 커버리지로 버그 수정 및 리팩토링이 수월 해, 유지보수가 높아진다는 큰 장점을 가지고 있습니다.
  • 반면에 테스트 코드를 작성하기 위한 러닝커브가 존재합니다.
    • Junit, Espresso 등의 API를 학습해야 함
  • 기획 변경에 따른 코드가 수정된다면, 지속적인 테스트 코드 수정 역시 불가피할 수 없습니다.