인프런의 Practical Testing: 실용적인 테스트 가이드를 듣고 작성한 글입니다.
테스트 코드는 말 그대로 프로덕션 코드를 테스트하기 위한 코드입니다. 그래서 사실 귀찮다고 느껴질 때가 많습니다.
프로덕션 코드를 작성하기에도 시간이 부족한데 테스트 코드를 왜 작성해야 할까요?
📌 테스트 코드가 없다면..
테스트 코드가 없다면, 프로덕션 코드를 수동으로 테스트하게 됩니다.
- 포스트맨을 이용
- 직접 웹사이트에 접속
간략하게 위와 같은 방법으로 만든 기능들을 테스트하게 됩니다. 그리고 추후 프로덕션 코드가 확장하게 됩니다. 기존 코드와 별개로 동작한다면 상관이 없지만, 겹치는 부분이 있다면 어떨까요?
테스트 영역이 겹치게 되거나, 코드가 변경된다면 기존 코드가 정상적으로 동작하는지 확인이 필요합니다. 즉, 다시 검증해야 하는 상황이 발생합니다.
프로덕션 코드는 시간이 지날수록 확장하게 되는데, 테스트 코드가 없다면 테스트에 소모되는 인력 또는 시간이 점점 늘어나게 됩니다. 또한 사람이 직접 테스트하게 되면 다양한 문제가 발생할 수 있습니다.
- 커버할 수 없는 영역 발생
- 경험과 감에 의존
- 늦은 피드백
- 유지보수 어려움
- 소프트웨어 신뢰 낮아짐
- 변화가 생기는 매 순간마다 발생할 수 있는 모든 Case를 고려해야 한다.
- 변화가 생기는 매 순간마다 모든 팀원이 동일한 고민을 해야 한다.
- 빠르게 변화하는 소프트웨어의 안정성을 보장할 수 없다.
📌 테스트 코드를 통해 얻고자 하는 것
테스트 코드를 작성한다면, 우리는 아래와 같은 결과를 얻을 수 있습니다.
- 빠른 피드백
- 자동화
- 안정감
그런데 테스트 코드 자체가 엉망이라면 어떨까요? 테스트 코드를 작성한다 하더라도, 프로덕션 코드의 안정성을 제공하지 못합니다. 또한 테스트 코드 자체가 유지보수하기 어려운 새로운 짐이 되고, 잘못된 검증이 이루어질 가능성이 생기게 됩니다.
테스트 코드를 올바르게 작성한다면, 다음과 같은 결과를 이끌어낼 수 있습니다.
- 자동화 테스트로 비교적 빠른 시간 안에 버그를 발견할 수 있고, 수동 테스트에 드는 비용을 크게 절약할 수 있다.
- 소프트웨어의 빠른 변화를 지원한다.
- 팀원들의 집단 지성을 팀 차원의 이익으로 승격시킨다.
테스트 코드를 작성하는 것은 물론 귀찮을 수 있습니다. 그래도 귀찮지만 해야 하는 작업입니다. 테스트 코드를 작성하는 것이 가까이 보면 느리게 나아가는 방법일 수 있지만, 소프트웨어의 전체적인 주기를 보면 멀리 봤을 때 가장 빠른 길일 수 있습니다.
결국 우리는 올바른 테스트 코드를 작성해야 합니다.
📌 테스트 케이스 세분화하기
요구사항이 있을 때 스스로 질문할 수 있어야 합니다. 이 요구사항이 과연 내가 실제 구현할 때 요구사항과 정확이 맞아떨어지는지, 또는 암묵적이어서 얘기를 안 한 것이 있거나 아직 도출되지 않은 것이 있는지 확인해야 합니다.
그래서 요구사항 정리하더라도 이 내용이 전부일지 계속 질문을 해야 합니다.
보통 테스트는 크게 두 가지로 분류할 수 있습니다.
- 해피 케이스 : 요구사항을 그대로 만족하는 경우
- 예외 케이스 : 요구사항을 만족하지 않는 경우
주문을 할 때 개수가 0개라면, 어떻게 대처할지에 대한 논의가 필요합니다. 또는 숫자가 음수일 때 어떻게 할지 바로 떠올리기 쉽진 않지만 충분히 일어날 수 있는 상황이므로 이러한 케이스를 막는 테스트와 프로덕션 코드가 있어야 합니다.
이런 케이스들을 테스트할 때는 경계값을 테스트하는 것이 좋습니다.
범위라면 이상/이하/초과/미만이 있고, 날짜, 경계 등이 있을 수 있습니다.
예를 들어, 숫자가 3 이상일 때 조건을 만족시킨다고 해봅시다. 이때 해피 케이스는 4, 5보다는 경계값인 3에 대한 테스트를 하는 것이 좋습니다. 반대로 예외 케이스는 0이나 -1보다는 2에 대한 테스트를 하는 것이 좋습니다.
📌 테스트하기 어려운 영역을 구분하고 분리하기
테스트하기 어려운 영역을 구분하고 분리하는 시야를 길러야 합니다.
어떤 영역이 테스트하기 어려운 영역인지 정의를 해보면 다음과 같습니다.
- 관측할 때마다 다른 값에 의존하는 코드
- 현재 날짜, 랜덤 값, 전역 변수, 사용자 입력
- 외부 세계에 영향을 주는 코드
- 표준 출력, 메시지 발송, 데이터베이스 기록

기존에 테스트 가능한 코드가 있었다고 가정해 봅시다. 그런데 어떤 조건이 추가되어서 테스트하기 어려운 코드가 들어왔습니다. 예를 들면, 현재의 날짜가 제약 조건으로 추가되었다고 해봅시다. 그렇다면 전체가 테스트 불가능한 상태가 됩니다.
public Order createOrder() {
LocalDateTime currentDateTime = LocalDateTime.now();
LocalTime currentTime = currentDateTime.toLocalTime();
if (currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요.");
}
return new Order(currentDateTime, beverages);
}
이 메서드에서 주문 시간 검증을 테스트하면 어떻게 될까요? 테스트를 수행할 때마다 값이 달라져서 테스트가 깨졌다가 성공하는 상황이 됩니다.
이때 우리는 테스트하기 어려운 영역을 외부로 분리해야 합니다.
LocalDateTime을 외부에서 받도록 파라미터를 분리하면 어떨까요?
public Order createOrder(LocalDateTime currentDateTime) {
LocalTime currentTime = currentDateTime.toLocalTime();
if (currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요.");
}
return new Order(currentDateTime, beverages);
}
이렇게 수정하면 이 메서드에서 주문 시간 검증 테스트가 가능해집니다. ‘이렇게 수정해도 될까요?’라는 질문이 생길 수 있습니다.
우리가 검증하고자 하는 것은 LocalDateTime.now()가 아니라 어떤 시간이 주어졌을 때 이 시간이 범위 안에 들어오는지가 중요합니다.
그렇기 때문에 우리가 테스트 코드 상에서 원하는 값을 넣어줄수록 설계를 변경하는 것이 중요합니다.
테스트하기 어려운 영역을 외부로 분리하면, 그다음으로 계층이 계속 존재할 것입니다. 이처럼 외부로 분리할수록 테스트 가능한 코드는 많아지게 됩니다. 이어지는 궁금증으로 ‘어느 외부 세계까지 분리해야 할까?’라는 생각이 들 수 있는데, 적당한 선에서 멈추면 됩니다.
결론적으로 테스트 어려운 영역을 구분하여 외부로 분리할수록 테스트 가능한 코드는 많아지게 됩니다.
📌 Test Driven Development
프로덕션 코드보다 테스트 코드를 먼저 작성하여 테스트가 구현 과정을 주도하는 방법론.
- RED : 실패하는 테스트 작성
- 프로덕션 코드 없이 작성
- GREEN
- 빠른 시간 내에 테스트를 통과하도록 작성
- 초록불을 보기 위해 어떻게든 작성
- REFACTOR
- 초록불을 유지하면서 구현 코드 개선
테스트를 먼저 작성하면, 다음과 같은 장점이 있습니다.
- 복잡도가 낮은, 테스트 가능한 코드로 구현할 수 있게 한다.
- 쉽게 발견하기 어려운 엣지 케이스를 놓치지 않게 해준다.
- 구현에 대한 빠른 피드백을 받을 수 있다
- 과감한 리팩토링이 가능해진다.
TDD는 개발하는 과정에서 우리의 관점을 변화시키게 합니다.

기존에는 테스트가 구현부 검증을 위한 보조 수단이라면, TDD에서는 테스트와 상호 작용하며 프로덕션 코드가 발전하게 됩니다.
즉, 클라이언트 관점에서 프로덕션 코드를 바라볼 수 있게 됩니다. 해당 객체를 사용하는 관점에서 바라볼 수 있습니다.
피드백
TDD는 프로덕션 코드에 대해서 자주, 빠르게 피드백을 받을 수 있습니다.
기능 구현을 먼저 하고 테스트를 작성하면 테스트 자체가 누락될 수 있습니다. 기간이 부족해서 테스트가 짤 시간이 없다면, 테스트가 중요하더라도 테스트가 누락될 가능성이 있습니다. 그럼 후에 테스트가 보장해주는 기능이 아니기 때문에 유지보수하기가 힘들게 됩니다. 또한 테스트가 늦게 만들어지면, 구현이 잘못됐을 경우 늦게 발견되기도 합니다.
TDD에서는 테스트를 먼저 작성하고 기능을 구현함으로써 복잡도가 낮고 테스트 가능한 코드로 구현할 수 있게 합니다.
위의 코드 예시에서 우리는 LocalDateTime.now()
를 파라미터로 분리하였습니다. 그런데 만약 테스트를 작성하지 않았다면, 이를 분리하겠다는 생각이 되게 늦게 들거나 테스트 작성 자체를 포기할 수도 있습니다. 테스트를 작성하였기 때문에 이 설계를 고민할 수 있게 됩니다.
📌 테스트는 문서다
테스트 코드는 문서로서의 역할을 할 수도 있습니다.
- 프로덕션 기능을 설명하는 테스트 코드 문서
- 다양한 테스트 케이스를 통해 프로덕션 코드를 이해하는 시각과 관점을 보완
- 어느 한 사람이 과거에 경험했던 고민의 결과물을 팀 차원으로 승격시켜, 모두의 자산으로 공유
DisplayName을 섬세하게
테스트의 DisplayName 중 음료 1개 추가 테스트
와 음료를 1개 추가하면 주문 목록에 담긴다.
라는 문장 중 어떤 것이 더 명확한가요? 후자가 더 명확하게 느껴집니다.
이처럼 문장 형태로 작성을 해야 어떤 행위를 가했을 때, 어떤 상태 변화가 있는지 알 수 있습니다. ~ 테스트
라고 적는 것은 지양하는 것이 좋습니다.
주문 생성하는 테스트를 작성한다고 가정해 봅시다. 이때 테스트 중 하나로 특정 시간 이전에 주문을 생성하면 실패한다.
로 DisplayName을 적을 수 있습니다. 그런데 더 나은 표현을 쓰자면 영업 시작 시간 이전에는 주문을 생성할 수 없다.
라고도 작성할 수 있습니다.
특정 시간 이전이라는 표현은 내가 개발하는 도메인에서 쓰는 용어라기보다는 일반적으로 사용하는 용어입니다. 하지만, 영업 시작 시간은 도메인에 부합하는 용어입니다. 이런 도메인 용어를 사용하여 풍부하게 표현하는 것이 좋습니다. 그래서 메서드 자체 관점보다는 도메인 정책의 관점으로 정책을 녹여 표현하는 것이 좋습니다.
추가적으로 테스트 현상을 중점으로 기술하면 좋지 않습니다. 특정 시간 이전에 주문을 생성하면 실패한다.
문장에서 사실 실패한다는 것은 테스트 내용과 무관합니다. 테스트가 성공/실패한다라고 이야기하지, 테스트하고자 하는 내용 자체와는 관계없는 단어입니다. 그래서 성공/실패 워딩을 피하고 도메인 용어를 사용하여 명확하게 표현하면 좋습니다.
📌 BDD
BDD는 Behavior Driven Development라고 합니다.
- TDD에서 파생된 개발 방법
- 함수 단위의 테스트에 집중하기보다, 시나리오에 기반한 테스트케이스 자체에 집중하여 테스트한다.
- 개발자가 아닌 사람이 봐도 이해할 수 있을 정도의 추상화 수준을 권장
Given / When / Then
- Given : 시나리오 진행에 필요한 모든 준비 과정
- When : 시나리오 행동 진행
- Then : 시나리오 진행에 대한 결과 명시, 검증
이 같은 과정을 통해서 DisplayName에서도 명확하게 정리할 수 있는 문장을 구성하기 쉬워집니다.
TDD와 BDD의 차이?
그렇다면 TDD는 메서드 단위로 테스트가 진행되고, BDD는 요구사항 시나리오 기반으로 테스트가 진행되는걸까요?
이 의견은 어느정도 맞지만, 두 방식의 차이가 테스트 단위의 차이로 드러나는 것은 아니라고 합니다.
TDD도 항상 메서드 단위에서만 사용되는 것이 아니라 더 큰 범위의 테스트 단위에서 사용될 수 있고, BDD도 작은 메서드 단위에서 시나리오 기반의 테스팅을 할 수 있습니다.
가장 큰 차이점은 관점의 차이입니다.
TDD는 보통 소프트웨어의 구현에 초점을 맞추고, BDD는 소프트웨어의 행동에 초점을 맞추어 사용자 요구사항의 관점에서 테스트를 진행합니다.
📌 Spring & JPA 테스트
레이어드 아키텍처는 스프링 MVC 기반에서 주로 사용되는 아키텍처입니다.

위 그림 말고도 다른 레이어로 구분될 수도 있습니다.
우리가 레이어를 구분하는 이유는 관심사를 분리하기 위해서입니다. 관심사를 분리하여 책임을 나누고 유지보수하기 용이하게 만듭니다.
이러한 레이어드 아키텍처가 테스트하기 복잡해 보일 수 있습니다. 하지만 레이어별로 뜯어서 접근하여 테스트한다면, 복잡하지 않습니다.
스프링에서도 테스트하기 어려운 부분을 분리해서 테스트하고자 하는 영역에 집중하여 명시적이고 이해할 수 있는 문서 형태로 테스트를 깔끔하게 작성하는 것은 동일합니다. 물론 레이어드 아키텍처가 아니더라도 모두 동일합니다.
그래서 스프링과 JPA라는 기술 자체보다는 어떻게 테스트할지에 집중해야 합니다.
A 모듈과 B 모듈이 있다고 가정해 봅시다. 모듈은 하나의 클래스가 될 수도 있고, 여러 개의 클래스가 모여있는 하나의 기능을 하는 모듈 단위가 될 수 있습니다.
근데 여러 객체가 협력해서 어떤 하나의 기능을 동작하게 한다던가, A 모듈과 B 모듈이 통합해서 동작했을 때 과연 어떻게 동작할지 예측할 수 있을까요? A + B
가 AB일지, BA일지, C일지 우리는 예측하기 어려울 수 있습니다.
그래서 우리는 통합 테스트가 필요합니다. 단위 테스트만으로 커버하기 어려운 영역들이 생기게 됩니다.
통합 테스트
- 여러 모듈이 협력하는 기능을 통합적으로 검증하는 테스트
- 일반적으로 작은 범위의 단위 테스트만으로는 기능 전체의 신뢰성을 보장할 수 없음
- 풍부한 단위 테스트 & 큰 기능 단위를 검증하는 통합 테스트
'스프링' 카테고리의 다른 글
@Modifying의 동작 알아보기 (0) | 2025.01.23 |
---|---|
[JPA] 연관관계의 주인이란? (0) | 2025.01.13 |
Mybatis vs JPA (0) | 2025.01.08 |
SELECT 작업에 트랜잭션은 필요할까? (0) | 2025.01.08 |
스프링 트랜잭션 (0) | 2025.01.07 |