TDD를 이용해 개발하기
카테고리: Java
업데이트:
이번에 우아한 테크 코스의 프리코스를 하면서 JUnit 5와 AssertJ를 이용해 자신이 구현한 기능을 테스트할 필요가 생겼습니다. 이번 프리코스를 하기전에 테스트 주도 개발이 무엇인지 개념을 간략히 알고 있었고 README.md에서 요구사항을 읽을때까지만 해도 자신감이 있었습니다. 하지만 막상 시작하니 테스트 기능을 어떻게 만들어야할지도 고민이고 기본적으로 주어지는 테스트 기능은 더더욱 모르겠어서 어디서부터 손대야할지 고민이었습니다. 그래도 아직 기간이 여유롭게 남아있고, 기능 구현은 그렇게 난이도가 높지 않아 조금만 고생하면 금방 구현할 수 있다는 자신감이 있어 테스트 주도 개발에 대해 먼저 공부하기로 결정했습니다.
TDD
TDD란?
TDD는 Test Driven Development의 약자로 테스트 주도 개발이라고 합니다. 작은 단위의 테스트 케이스를 작성하고, 이를 통과하는 코드를 추가하는 단계를 반복하며 구현하는 방법론 입니다. TDD를 가장 쉽게 표현한 것이 “Red-Green-Refactor” 개발 주기로 아래 그림과 같습니다.

- Red 단계에서는 실패하는 테스트 코드를 먼저 작성합니다.
- Green 단계에서는 테스트 코드를 성공시키기 위한 실제 코드를 작성합니다.
- Yellow 단계에서는 중복 코드를 제거하고 일반화 등의 리팩토링을 수행합니다.
TDD의 장점
TDD를 통해 얻을 수 있는 장점은 단위 테스트로부터 얻어지는 장점과 테스트를 먼저 작성하는 방식으로부터 얻어지는 장점으로 구분이 가능합니다.
단위 테스트의 장점
- 동작하는 코드에 대한 자신감을 가질 수 있습니다.
- 회귀 테스트를 통해 자유롭게 리팩토링 할 수 있습니다.
- 실제 코드에 좋지 않은 설계 구조를 손쉽게 파악할 수 잇습니다.
- 디버깅 하는 시간이 현저히 줄어듭니다.
- 잘못된 코드에 대해 빠른 오류 확인이 가능합니다.
테스트를 먼저 작성하는 방식의 장점
- 실제 코드에 대한 명확한 처리를 설계함으로써 과도한 설계를 피할 수 있습니다.
- 사전에 작성된 테스트 코드는 요구 사항을 구체화하는 문서로써 활용될 수 있습니다. (실행 가능한 문서)
JUnit
JUnit이란?
JUnit은 현재 전 세계적으로 가장 널리 사용되는 Java 단위 테스트 프레임워크 입니다.
각 언어별로 단위 테스트 프레임워크는 다음과 같습니다.
| xUnit 이름 | 해당언어 |
|---|---|
| CUnit | C |
| CppUnit | C++ |
| PHPUnit | PHP |
| PyUnit | Python |
| JUnit | Java |
JUnit5 Annotation
| JUnit5 어노테이션 | 내용 |
|---|---|
| @Test | 테스트 Method임을 선언함. |
| @ParameterizedTest | 매개변수를 받는 테스트를 작성할 수 있음. |
| @RepeatedTest | 반복되는 테스트를 작성할 수 있음. |
| @TestFactory | @Test로 선언된 정적 테스트가 아닌 동적으로 테스트를 사용함. |
| @TestInstance | 테스트 클래스의 생명주기를 설정함. |
| @TestTemplate | 공급자에 의해 여러 번 호출될 수 있도록 설계된 테스트 케이스 템플릿임을 나타냄. |
| @TestMethodOrder | 테스트 메소드 실행 순서를 구성하는데 사용함. |
| @DisplayName | 테스트 클래스 또는 메소드의 사용자 정의 이름을 선언할 때 사용함. |
| @DisplayNameGeneration | 이름 생성기를 선언함. 예를 들어 ‘_‘를 공백 문자로 치환해주는 생성기가 있음. ex ) new_test -> new test |
| @BeforeEach | 모든 테스트 실행 전에 실행할 테스트에 사용함. |
| @AfterEach | 모든 테스트 실행 후에 실행한 테스트에 사용함. |
| @BeforeAll | 현재 클래스를 실행하기 전 제일 먼저 실행할 테스트 작성하는데, static로 선언함. |
| @AfterAll | 현재 클래스 종료 후 해당 테스트를 실행하는데, static으로 선언함. |
| @Nested | 클래스를 정적이 아닌 중첩 테스트 클래스임을 나타냄. |
| @Tag | 클래스 또는 메소드 레벨에서 태그를 선언할 때 사용함. 이를 메이븐을 사용할 경우 설정에서 테스트를 태그를 인식해 포함하거나 제외시킬 수 있음. |
| @Disabled | 이 클래스나 테스트를 사용하지 않음을 표시함. |
| @Timeout | 테스트 실행 시간을 선언 후 초과되면 실패하도록 설정함. |
| @ExtendWith | 확장을 선언적으로 등록할 때 사용함. |
| @RegisterExtension | 필드를 통해 프로그래밍 방식으로 확장을 등록할 때 사용함. |
| @TempDir | 필드 주입 또는 매개변수 주입을 통해 임시 디렉토리를 제공하는데 사용함. |
Jupiter API Assert
| Assert | 설명 |
|---|---|
| assertEquals(expected, actual) | expected값과 actual 값이 일치하는지 확인 |
| assertNotEquals(expected, actual) | expected값과 actual 값이 일치하지 않는지 확인 |
| assertTrue(condition) | condition이 true 인지 확인 |
| assertFalse(condition) | condition이 false인지 확인 |
| assertNull(actual) | actual이 null인지 확인 |
| assertNotNull(actual) | actual이 null이 아닌지 확인 |
| assertAll() | assert로 여러개의 검증을 할 때 사용 |
| assertThrows(Exception.class, executable) | 발생한 예외가 주어진 Exception과 동일한 클래시인지 확인 |
| assertTimeout(milliSec, executable) | 특정 시간 안에 실행이 되었는지 확인 |
AssertJ
AssertJ란?
AssertJ는 다양한 assertion을 제공하는 자바 라이브러리입니다. 에러 메시지와 테스트 코드의 가독성을 매우 높여주고 각자 좋아하는 IDE에서 쓰기 굉장히 쉽습니다. Junit에서 제공하는 assertEquals에 비해 훨씬 가독성이 올라갑니다. Junit의 assertEquals의 경우 인자 순서를 헷갈릴 가능성이 큽니다.
assertEquals(expected,actual); // JUNIT
assertThat(actual).isEqualTo(expected); // AssertJ
AssertJ Assertion
as Assertion
assertion이 수행될 때 상황을 설명하는 as Assertion을 이용합니다. 이때 as를 Assertion 이전에 호출하애합니다.
TolkienCharacter frodo = new TolkienCharacter("Frodo", 33, Race.HOBBIT);
assertThat(frodo.getAge()).as("check %s's age", frodo.getName())
.isEqualTo(100);
filtering Assertion
iterables나 arrays에 람다식을 이용해 filtering 할 수 있습니다.
@Test
void filter_test() {
List<Human> list = new ArrayList<>();
Human kim = new Human("Kim", 22);
Human park = new Human("Park", 25);
Human lee = new Human("Lee", 22);
Human amy = new Human("Amy", 25);
Human jack = new Human("Jack", 22);
list.add(kim);
list.add(park);
list.add(lee);
list.add(amy);
list.add(jack);
assertThat(list).filteredOn(human -> human.getName().contains("a"))
.containsOnly(park, jack);
}
객체의 필드
다음 코드와 같이 객체의 해당 필드의 값을 비교할 수 있습니다. 이때 not, in, notIn 등을 이용해 필드의 값을 검증할 수 있습니다.
@Test
void filter_test2() {
List<Human> list = new ArrayList<>();
Human kim = new Human("Kim", 22);
Human park = new Human("Park", 25);
Human lee = new Human("Lee", 25);
Human amy = new Human("Amy", 22);
Human jack = new Human("Jack", 22);
list.add(kim);
list.add(park);
list.add(lee);
list.add(amy);
list.add(jack);
// 해당 필드의 값을 가지고 있는지 검사
assertThat(list).filteredOn("age", 25).containsOnly(park, lee);
// 해당 필드의 값을 가지고있지 않는지 검사
assertThat(list).filteredOn("age", notIn(25)).containsOnly(park, lee);
}
필드 추출하기
// 필드를 추출해 검증하기
assertThat(actual).extracting("fieldName").contains("Kim", "Park", "Lee", "Amy", "Jack");
// 여러 필드를 튜블로 추출해 검증하기
assertThat(actual).extracting("name", "age")
.contains(tuple("Kim", 22),
tuple("Park", 25),
tuple("Lee", 25),
tuple("Amy", 22),
tuple("Jack",22));
예외 테스트
assertThat(thrown)
.isInstanceOf(Exception.class) // 예외 클래스 종류
.hasMessageContaining("Except!"); // 예외 메시지 검사
출처
https://www.samsungsds.com/kr/insights/Test-Driven-Development.html?referrer=https://wooaoe.tistory.com/33
https://insight-bgh.tistory.com/507
http://joel-costigliola.github.io/assertj/assertj-core.html
댓글남기기