본문 바로가기
시리즈/소프트웨어 공학

[TDD] 테스트 주도 개발의 의미와 방법에 대해서

by 되고싶은노력가 2024. 10. 24.

들어가며.

개발을 하면서 TDD 라는 용어는 한 번쯤은 들어봤을거라 생각하는데요. 이번에 프리코스 2주차를 시작하면서 JUnit 5와 AssertJ 를 이용하여 개발을 진행해보라는 요구 사항을 접하게 되었습니다. 말로는 많이 들어봤지만 개발이 바쁘다보니 실제로 적용해본 적은 없는데요. 이번 기회에 TDD 에 대해 정리하고 JUnit 과 AssertJ 에 익숙해지고자 작성하게 되었습니다.


TDD 에 대한 간략한 소개.

TDD 는 Test Driven Development 의 약자로 켄트백이 1999년 익스트림 프로그래미의 일부로 제안하며 알려졌습니다. 지금은 많은 분들이 익스트림 프로그래밍은 뭔지 몰라도 TDD 는 알 만큼 유명해졌는데요. TDD 는 동작하는 코드를 작성하기 이전에 테스트를 먼저 작성하고, 그 테스트를 통과하는 코드를 작성함으로써 테스트된 동작하는 코드를 얻는 개발 방법입니다.

 

TDD 는 세 가지 단계를 한 사이클로 돌게 됩니다.

  • 테스트를 작성한다. (빨간 막대)
  • 실행 가능하게 만든다. (초록 막대)
  • 올바르게 만든다.

흔히 red - green - reactoring cycle 이라고 하는데요. 컴파일조차 되지 않는 코드를 대상으로 먼저 테스트를 작성하고, 그 다음 테스트 코드가 컴파일되고 실행까지 되도록 코드를 작성한 후 테스트가 통과되면 테스트의 보호 아래 코드를 다듬는 것입니다.


단위 테스트의 중요성과 특징.

# 중요성

단위 테스트를 작성해야 하는 이유는 정말 많은데 그 중 핵심적인 이유 세 가지가 있다.

  • 코드를 수정하거나 기능을 추가할 때 수시로 빠르게 검증 할 수 있다.
  • 리팩토링 시에 안정성을 확보할 수 있다.
  • 개발 및 테스팅에 대한 시간과 비용을 절감할 수 있다.

# 특징

그렇다고 무작정 테스트를 작성하는 것은 좋지 않다. 좋은 테스트의 특징은 FIRST 라는 5가지 규칙을 따른다.

  • Fast (빠르게) : 테스트는 빠르게 동작하여 자주 돌릴 수 있어야 한다.
  • independent (독립) : 각각의 테스트는 독립적이며 서로 의존해서는 안 된다.
  • Repeatable (반복) : 어느 환경에서도 반복 가능해야 한다.
  • Self-Validating (검증) : 테스트는 성공 또는 실패로 boolean 값으로 결과를 내어 자체적으로 검증되어야 한다.
  • Timely (적시) : 테스트는 실제 코드를 구현하기 직전에 구현해야 한다. 

테스트 주도 개발 이유와 방법.

이유는 위에서 언급한 단위 테스트의 중요성과 비슷하다. TDD 의 궁극적인 목표는 작동하는 깔끔한 코드를 작성하는 것이 그 목표이다. TDD 의 개발 단계에는 리팩토링이 있는데, 이 리팩토링 과정을 거치면서 중복된 코드들이 제거되고 복잡한 코드는 깔끔하게 정리된다. 처음에는 귀찮고 개발이 느리다고 느낄 수 있지만 장기적으로 본다면 개발 비용을 아껴주게 된다. 그리고 그 중에서도 실패 테스트부터 작성해야한다. 즉, 순차적으로 실패하는 테스트를 먼저 작성하고, 오직 테스트가 실패할 경우에만 새로운 코드를 작성해야 한다.

 

현실적으로 프로덕션 코드를 작성하고 나면 그 뒤에 테스트 코드를 작성하는 과정은 너무나도 귀찮게 느껴질 것이다. 이미 개발이 완료되었기에 심리적으로 꺼려지기도 하며 완성된 코드에서 발생하는 테스트의 경우의 수가 너무나 많기 때문이다.

 

# 방법

TDD 개발의 방법론의 프로그래밍 순서는 매우 단순하다.

  1. 실패하는 작은 단위 테스트를 작성한다.
  2. 빨리 테스트를 통과하기 위한 프로덕션 코드를 작성한다. 이를 위해 가짜 구현 등을 작성할 수도 있다.
  3. 테스트 코드를 작성한다. 실패 테스트가 없을 경우에만 성공 테스트를 작성한다.
  4. 새로운 테스트 코드를 통과하기 위해 프로덕션 코드를 추가 / 수정 한다.
  5. 1 ~ 4 단계를 반복하여 테스트를 케이스를 작성한다.
  6. 개발된 코드에 대해 리팩토링을 진행한다.

위 방법과 개발에 대한 접근 방법은 망나니개발자 님의 블로그를 참고했다.

 

# 접근 방법

1) 가짜로 구현하기

실패하는 테스트를 가장 빠르게 구현하는 방법은 아무 상수 값이나 반환하도록 하는 것이다. 그리고 테스트가 통과하면 단계적으로 상수를 변수로 변형한다.

@Test
public void 곱하기테스트() {
    // given
    
    // when
    final int result = multiply(2, 3);
    
    // then
    assertThat(result).isEqualTo(6);
}

 

해당 테스트를 통과하는 가장 빠른 방법은 6을 반환하는 것이다.

public int multiply(final int num1, final int num2) {
    return 6;
}

 

가짜 구현으로 개발하면 두 가지 효과를 얻을 수 있다.

  • 빨간 막대와 초록 막대 상태는 완전 다르기에 어느 위치인지 알고 거기부터 리팩토링해 나갈 수 있다.
  • 구체적인 예에서 일반화로 바꾸면서 불필요한 고민으로 혼동되는 일을 예방할 수 있다.

 

2) 삼각 측량

삼각 측량은 테스트 주도로 추상화된 과정을 일반화하는 과정이다. 삼각 측량 방법은 테스트 예시가 2개 이상일 때에만 추상화를 한다.

@Test
public void 곱하기테스트() {
    // given
    
    // when
    final int result1 = multiply(2, 3);
    final int result2 = multiply(4, 7);
    
    // then
    assertThat(result1).isEqualTo(6);
    assertThat(result2).isEqualTo(28);
}

 

 

3) 명백하게 구현하기

명백하게 구현하는 방법은 가짜 구현이나 삼각 측량 방법을 사용하지 않고 바로 정답을 구현하는 방법이다. 매우 쉬운 구현 단계에서는 이 방법이 시간을 절약할 수 있다.

public int multiply(final int num1, final int num2) {
    return num1 * num2;
}