TL;DR
테스트 코드도 하나의 명세서처럼 읽혀야 한다는 피드백을 받고 DCI 패턴을 적용해 보았다.
JUnit에서 @Nested 와 @DisplayNameGeneration 애노테이션을 활용해 테스트 흐름을 구조화했고, 그 결과 테스트의 가독성이 좋아지고 유스케이스 분리가 훨씬 쉬워졌다.
왜 고쳐야 하는데?
사이드 프로젝트에서 테스트 코드 리뷰 중 이런 피드백을 받았다.
“메서드명에 테스트에서 검증하고자 하는 바가 잘 드러나지 않는다.”
”테스트코드는 사용 설명서로도 작용할 수 있어야 한다.”
사실 되돌아보면 테스트 메서드 이름에 그렇게까지 신경 써야 하나 싶어서 나름 의미만 통하면 괜찮다고 생각하며 대충 지어왔던 것 같다.
코드만으로도 어느 정도 흐름 파악은 되겠지만, 그 테스트가 무엇을 어떤 맥락에서 검증하고자 했는가는 작성자가 아니면 정확히 알기 어렵다는 걸 놓치고 있었다.
그럼 어떻게 고쳐야 하지
그렇다면 어떻게 해야 테스트를 더 명확하게 명세처럼 읽히게 만들 수 있을까?
좋은 테스트란 테스트 코드만으로도 요구사항이나 맥락이 한눈에 들어오는 것이라고들 한다.
그런데 기존에 내가 했던 기존 테스트 코드는 조건이나 예상 결과도 모호했고, 같은 결과라도 흐름이 잘 읽히지 않았던 코드라고 생각한다.
그러던 중 DCI 패턴이라는 것을 알게 되었고 내 테스트 코드에 적용해 보기로 했다.
DCI 패턴이란?
DCI 패턴은 아래의 3단계 구조로 테스트를 구성하는 방식이다.
- Describe: 테스트 대상
- Context: 테스트 대상의 상황
- It: 테스트 대상의 행동
주로 kotest, mocha 같은 테스트 프레임워크에서 많이 사용되며 테스트를 유스케이스 흐름처럼 읽을 수 있게 해주는 패턴이다.
describe("회원 가입 시") {
context("예외가 발생한다") {
it("중복된 아이디인 경우") {...}
it("이메일 형식이 잘못된 경우") {...}
}
}
테스트 코드를 계층 구조로 만들어주기 때문에 결과 출력도 트리 구조로 정리되어 가독성이 좋고, 한 번에 하나의 스코프만 집중할 수 있다는 장점이 있다.
내가 사용하는 환경은 Java + JUnit 이라 이렇게 문법적으로 간결하게 작성하긴 어려웠지만, @Nested, @DisplayName 두 가지 애노테이션을 사용해 어느 정도 구조화할 수 있었다.
JUnit에서 적용 하기
기존 코드
@Test
@DisplayName("유효하지 않은 아이디의 경우 예외 발생")
void throwExceptionByInvalidId() {...}
→ 어떤 점이 유효하지 않은 건지, 어떤 예외가 나는지 모호함
@Test
@DisplayName("존재하지 않는 유저 ID 로 충전을 시도한 경우 예외 발생")
void chargeWithInvalidUserId_returnException() {...}
→ 조건은 명확해졌지만 여전히 어떤 예외가 발생하는지 모호함
적용 후
@Nested
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class 포인트_충전_시 {
@Nested
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class 충전에_실패한_후_400_Bad_Request_예외가_발생한다 {
@Test
@DisplayName("존재하지 않는 유저 아이디라면")
void 존재하지_않는_유저_아이디라면() {...}
테스트 코드 메서드명만 읽어도 유스케이스가 자연스럽게 드러남
포인트 충전 시 → 실패한 후 400 Bad Request 예외가 발생한다 → 존재하지 않는 유저 아이디라면
적용해보고
이렇게 DCI 패턴을 내 테스트 코드에 적용해 보았다.
직접 적용해 본 결과 아래와 같은 장단점이 있다고 생각되었다.
일단 장점으로는 테스트가 명세서처럼 읽히면서 가독성이 좋아졌고, 유스케이스 단위로 케이스를 나누기 쉬워졌다는 점이 있다.
하지만 단점도 분명 존재했다. Java + JUnit 환경에서는 중첩이 많아지면 오히려 구조 자체가 복잡해질 수 있다고 생각되었다. 그래서 2~3단계 수준으로 조절하던가, 테스트 클래스를 유스케이스 단위로 분리해 주는 것도 괜찮은 선택이라고 생각했다.
마무리
처음에는 그냥 테스트 메서드명을 좀 더 읽기 쉽게 바꿔보자는 가벼운 생각이었다.
그런데 구조 자체를 바꿔보니 테스트 코드가 요구사항 명세서처럼 읽히는 구조가 되었다.
테스트도 결국 코드고, 그 코드도 읽히는 것이 중요하다.
앞으로는 조금 더 읽는 사람을 배려하는 테스트를 작성해야겠다는 생각이 들었고, 나처럼 테스트 케이스 나누기나 메서드 이름 짓기가 고민인 사람이라면 DCI 패턴을 한 번쯤 적용해 보는 걸 추천하고 싶다.