외부활동/우아한테크코스 [프리코스]

테스트주도 개발 시작하기 - 챕터 9. 테스트 범위와 종류

softmoca__ 2025. 11. 6. 22:30
목차

https://product.kyobobook.co.kr/detail/S000001248962

 

테스트 주도 개발 시작하기 | 최범균 - 교보문고

테스트 주도 개발 시작하기 | 작동하는 깔끔한 코드를 만드는 데 필요한 습관 - JUnit 5를 이용한 테스트 주도 개발 안내 - 테스트 작성과 설계를 위한 대역 - 테스트 가능한 설계 방법 안내 - 유지

product.kyobobook.co.kr

 

1. 테스트 범위

1-1. “테스트 범위”

테스트 범위 = 테스트를 할 때 실제 시스템 구성요소를 어디까지 묶어서 하나의 시나리오로 돌릴 것인가

일반적인 웹 서비스 기준으로 테스트에 등장할 수 있는 구성요소

  • 브라우저 / 모바일 앱 (프런트)
  • HTTP 서버, 웹 프레임워크 (Spring MVC 등)
  • 서비스, 도메인, 리포지토리 코드
  • DB, 캐시, 메시지 큐, 외부 API 서버
  • 설정 파일, 스키마, HTML/JS/CSS 등 프런트 리소스

하나의 “회원가입 기능”이 제대로 동작하려면 실제로는 이 모든 게 맞물려야 한다.

근데 테스트는 항상 이 모든 걸 한 번에 다 묶어서 테스트할 필요는 없다

그래서 “테스트 범위”라는 말을 쓰는 거고,

대표적으로

  • 기능(E2E) 테스트
  • 통합 테스트
  • 단위 테스트

2. 기능 테스트 / E2E 테스트

2-1. 기능 테스트(Functional Test) 정의

기능 테스트는 간단히 사용자 입장에서, 시스템이 제공하는 기능이 올바르게 동작하는지 확인하는 테스트.

예를 들어 회원가입 기능을 기능 테스트로 검증하려면:

  • 브라우저나 모바일 앱에서
  • 회원가입 화면을 열고
  • 필수 필드를 채우고
  • “가입” 버튼을 누른 다음
  • DB에 회원이 잘 들어갔는지, 화면에 성공 메시지가 나오는지 등을 확인

즉, 끝에서 끝까지(End to End) 실제 사용 플로우를 따라가기 때문에 기능 테스트는 곧 E2E 테스트라고도 불림

2-2. 특징

  • 범위가 가장 넓다
    • 브라우저/앱 ~ 서버 ~ DB ~ 외부 시스템까지 실제로 얽힘
  • 사용자 시나리오 그대로 테스트한다
    • QA가 수동으로 하는 시나리오 테스트도 여기에 포함 가능
  • 준비 비용이 크다
    • 테스트 데이터를 준비하고,
    • 웹 서버/앱을 띄우고,
    • 프론트, 백엔드, DB 설정을 다 맞춰야 함
  • 실행 시간이 길다
    • UI 오픈, HTTP 왕복, DB IO 등등

그래서 E2E 테스트는 “다양한 예외 케이스를 빡빡하게 커버”하는 용도라기보다,

  • 주요 시나리오 (해피 패스, 대표 실패 케이스 등)
  • 릴리즈 전 최종 검증 에 가깝게 가져가야 한다

3. 통합 테스트

3-1. 통합 테스트

시스템의 여러 구성 요소들이 서로 잘 연동되는지 확인하는 테스트.

예를 들어 웹 앱에서

  • 서비스 클래스 + 리포지토리 + DB
  • REST 클라이언트 + 외부 서버 (대역 포함)
  • 스프링 컨테이너 + 설정 + Bean 초기화

이런 정도의 범위를 묶어 한 번에 테스트하는 걸 의미

  • 기능 테스트
    • 사용자 관점, 브라우저/앱까지 포함
  • 통합 테스트
    • 주로 서버-side 코드 관점
    • 컨트롤러~DB 또는 하나의 모듈~외부 시스템 정도 범위를 테스트

3-2. 예시: 서비스 레벨 통합 테스트

“회원가입 서비스(UserRegister)” 같은 걸 테스트할 때

  • 스프링 부트 컨테이너를 띄우고 (실제 빈 등록)
  • 진짜 DB(H2, MySQL 등)에 연결한 뒤
  • UserRegister.register()를 호출하고
  • DB에 데이터가 잘 들어갔는지 JdbcTemplate 로 직접 확인

→ 스프링 + DB 통합 테스트

4. 단위 테스트

한 클래스, 한 메서드 같은 작은 코드 단위가 기대대로 동작하는지 확인하는 테스트.

특징:

  • 일반적으로 외부 의존성은 대역(Stub/Fake/Mock) 으로 대체
  • 테스트 대상 메서드의 입력/출력(또는 상태 변화)를 중심으로 검증
  • 테스트 실행 속도가 매우 빠름(서버/컨테이너/DB 안 띄워도 되기 때문)

5. 테스트 범위 간 차이

5-1. 준비 비용

  • 기능 테스트
    • 웹 서버/앱 구동
    • 브라우저/디바이스 준비
    • 실 DB/외부 시스템까지 세팅 필요
    • → 준비 비용 가장 큼
  • 통합 테스트
    • DB, 캐시, 메시지 브로커, 스프링 컨테이너 등 필요
    • 웹 UI까지는 안 건드릴 수도 있지만, 그래도 환경 세팅이 많음
  • 단위 테스트
    • JUnit + 코드만 있으면 됨
    • 외부 의존성은 대역으로 처리
    • → 준비 비용 최소

5-2. 실행 속도

  • 기능 테스트: 제일 느림
    • 브라우저 구동/렌더링, 전체 HTTP, DB 등
  • 통합 테스트: 그 다음
    • 스프링 컨테이너 초기화, DB IO
  • 단위 테스트: 압도적으로 빠름
    • 순수 자바 코드 실행 + 메모리 상 대역만 사용

가능하면 많은 상황을 단위 테스트에서 다루고, 통합/기능 테스트는 “핵심 시나리오” 위주로 가져가라.

5-3. 표현할 수 있는 상황

  • 외부 시스템과 연동하는 기능은 통합/기능 테스트 만으로는, 특정 예외 상황(타임아웃, 에러 코드 등)을 만들기 어렵다.
  • 예를 들어 “외부 서버가 계속 타임아웃 나는 상황”을 실제 서버에게 매번 부탁할 수는 없음.
  • 이런 경우엔 단위 테스트 + 대역(Stub/Mock/WireMock) 을 조합해서 상황을 마음대로 구성해야함.

6. 테스트 범위 vs 테스트 개수 및 시간 전략

6-1. 모든 걸 통합/기능 테스트로 커버하면?

  • 예외 상황까지 전부 통합/기능 테스트로만 커버하면, 테스트 개수는 상대적으로 적게 보일 수 있다.
    • 한 통합 테스트가 많은 로직을 한 번에 커버하기 때문
  • 하지만:
    • 테스트 한 개당 실행 시간이 길다.
    • 설정/데이터 정리도 많이 필요합니다.
    • 전체 테스트 수행 시간이 길어져서 “귀찮아서 테스트 안 돌리게 되는” 상황이 생기기 쉽다.

6-2. 단위 테스트 중심 전략

  • 다양한 상황(예외, 경계값, 복잡한 조합) 은 가급적 단위 테스트로 커버
  • 통합/기능 테스트는
    • 핵심 플로우(해피 패스 + 대표 실패 케이스) 위주
    • 프레임워크 설정, DB 연동, 실제 API JSON 포맷 같은 것 위주 검증

이렇게 하면 전체 테스트 수는 많아질 수 있지만, 대부분이 빠른 단위 테스트이기 때문에 피드백 속도는 오히려 빨라진다.

7. 외부 연동이 필요한 테스트 예

7-1. 스프링 부트와 DB 통합 테스트

7-1-1. 구성

프링 부트 애플리케이션에서 아래 Bean 들이 등록되어 있다고 가정

  • UserRegister (회원 가입 서비스)
  • SimpleWeakPasswordChecker (약한 비밀번호 체크 구현)
  • UserRepository (Spring Data JPA)
  • VirtualEmailNotifier (테스트용 이메일 알림 구현)

UserRegisterIntTest 라는 통합 테스트 클래스에서:

  • @SpringBootTest 로 스프링 컨테이너를 띄우고
  • @Autowired 로 UserRegister 와 JdbcTemplate 를 주입받아요

7-1-2. 시나리오 1 – 중복 ID 존재 시 예외

통합 테스트에서 “이미 같은 ID가 있을 때 가입하면 예외가 발생” 하는지 확인하기 위해:

  1. 상황 만들기
    • JdbcTemplate.update() 로 user 테이블에 ID가 "cbk"인 행을 강제로 insert
    • 실제로 이미 데이터가 있어도 에러 안 나게 ON DUPLICATE KEY UPDATE 같이 처리
  2. 실행
    • register.register("cbk", "strongpw", "email@...") 호출
  3. 결과 확인
    • DupIdException 이 발생하는지 assertThrows() 로 검증
  • 통합 테스트에서는 실제 DB를 조작해서 상황을 만들고 다시 DB 상태를 조회하거나 예외를 확인해 결과를 검증

7-1-3. 시나리오 2 – 없는 ID면 정상 저장

두 번째 테스트는 “ID가 없으면 잘 저장되는지” 확인

  1. 상황 만들기
    • delete from user where id = 'cbk' 같은 쿼리로 해당 ID 레코드 삭제
  2. 실행
    • register.register("cbk", "strongpw", "email@...")
  3. 결과 확인
    • select * from user where id='cbk' 로 조회해
    • email 컬럼 값이 기대한 값인지 assertEquals

여기서 강조하는 점 통합 테스트는 실제 DB를 사용하므로, 같은 테스트를 여러 번 돌려도 결과가 같도록 DB 상태를 테스트 안에서 직접 통제해야 한다.

7-1-4. 단위 테스트와 비교

같은 요구사항을 단위 테스트로 하면 UserRepository 를 진짜 DB가 아닌 MemoryUserRepository (Fake)로 사용

  • 중복 ID 상황:
    • Fake repo에 fakeRepository.save(new User("id", ...)) 한 번 넣어두고,
    • userRegister.register("id", ...) 에서 예외 발생 여부만 확인
  • ID가 없는 상황:
    • Fake repo가 비어 있는 상태에서 register() 호출 후
    • Fake repo 안 데이터만 검사하면 됨
  • 통합 테스트
    • DB insert/delete 쿼리 직접 실행 + 컨테이너 구동
    • 상대적으로 느림, 설정 부담 큼
  • 단위 테스트
    • 메모리에서 객체만 만지면 됨
    • 매우 빠르고 가볍다

7-2. WireMock을 이용한 REST 클라이언트 테스트

7장에서 만들었던 CardNumberValidator 를 실제 HTTP 통신까지 포함한 통합 테스트로 검증

7-2-1. CardNumberValidator 코드 요약

CardNumberValidator 는 외부 카드사 API에 HTTP POST를 보내서 카드번호 유효성을 확인하는 역할

  • 생성자에서 서버 주소를 받는다.
  • validate(cardNumber) 에서 POST {server}/card 로 카드번호를 text/plain body로 전송
    • 응답 body 문자열이
      • "ok" → VALID
      • "bad" → INVALID
      • "expired" → EXPIRED
      • "theft" → THEFT
      • 그 외 → UNKNOWN
    • 3초 타임아웃 설정, 타임아웃 시 TIMEOUT, 기타 예외는 ERROR 등으로 매핑

이 코드는 실제 HttpClient 를 사용해 요청, 응답을 처리합니다.

7-2-2. 문제의식

이걸 통합 테스트하려면 실제 카드사 서버가 있어야 하고, 응답을 "ok", "expired", "bad", "theft", 타임아웃 등으로 원하는 대로 제어할 수 있어야 한다.

하지만 현실적으로 외부 업체에 “테스트마다 5초씩 늦게 응답해주세요” 라고 부탁할 수 없고, 운영 환경과 테스트 환경을 구분하기도 쉽지 않을 수 있음.

그래서 WireMock 을 사용해 HTTP 서버 자체를 대역으로 띄움

7-2-3. WireMockServer 테스트 구조

CardNumberValidatorTest의 구조

  1. @BeforeEach
    • wireMockServer = new WireMockServer(port) 로 서버 생성
    • wireMockServer.start() 로 실제 HTTP 서버 시작
  2. 각 @Test 메서드 안에서
    • stubFor(...) 로 요청 → 응답 규칙 정의
    • CardNumberValidator 를 new CardNumberValidator("<http://localhost:8089>") 처럼 생성
    • validate("1234567890") 호출
    • 결과 enum 이 기대값인지 assertEquals 로 검증
  3. @AfterEach
    • wireMockServer.stop() 으로 서버 정리

7-2-4. 시나리오 예시 – 유효 카드 / 타임아웃

  1. 유효한 카드번호 테스트
  • WireMock 설정:
    • URL /card
    • POST 요청
    • Body "1234567890" 이 들어오면
    • Header Content-Type: text/plain, Body "ok" 으로 응답하도록 stub
  • CardNumberValidator가 이 서버로 요청을 보내면 응답 body "ok" → CardValidity.VALID 가 되어야 함
  • 테스트에서 assertEquals(CardValidity.VALID, validity) 확인
  1. 타임아웃 상황 테스트
  • WireMock 설정:
    • URL /card, POST 요청에 대해 응답 지연 withFixedDelay(5000) (5초) 같은 식으로 설정
  • CardNumberValidator는 3초 타임아웃으로 설정되어 있으므로
    • HttpTimeoutException 발생 → 내부에서 CardValidity.TIMEOUT 반환해야 함
  • 테스트에서 assertEquals(CardValidity.TIMEOUT, validity) 로 검증

WireMock으로 HTTP 서버 자체를 대역으로 띄우면,실제 네트워크 IO + HTTP 클라이언트 코드까지 포함한 테스트를 원하는 상황(응답/지연/에러) 별로 마음대로 구성할 수 있다.

7-3. 스프링 부트 내장 서버를 이용한 API 기능 테스트

“스프링 부트 내장 톰캣 + TestRestTemplate”를 사용해 실제 HTTP API 레벨에서 기능(E2E) 테스트

7-3-1. 시나리오: 약한 비밀번호일 때 회원가입 API 응답 검증

  • 모바일 앱이 POST /users API를 통해 회원가입을 요청
  • 비밀번호가 약하면 서버가 400(BAD_REQUEST)와 함께 에러 정보를 JSON으로 응답
@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
public class UserApiE2ETest {
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void weakPwResponse() {
        // 1) JSON 요청 바디 작성
        // 2) RequestEntity.post(URI.create("/users")) 로 요청 객체 생성
        // 3) restTemplate.exchange(...) 로 실제 HTTP 호출
        // 4) 상태코드가 400인지, 응답 body에 예외명이 포함되는지 검증
    }
}
  • @SpringBootTest(webEnvironment = RANDOM_PORT)
    • 내장 서버(예: 내장 톰캣)를 랜덤 포트로 띄워서 실제 웹 애플리케이션 구동
  • TestRestTemplate
    • 스프링 부트가 테스트용으로 제공하는 HTTP 클라이언트
    • 위 랜덤 포트에 자동으로 붙어서 요청을 날림
  • 테스트는 실제로 HTTP 요청을 보내고 응답을 받기 때문에 컨트롤러, 서비스, 예외 핸들러, JSON 직렬화 등 웹 API 레이어 전체를 한 번에 검증하는 기능(E2E) 테스트가 됨

핵심정리

  1. 테스트 범위는 세 가지
    • 기능(E2E) 테스트: 브라우저/앱~서버~DB~외부 시스템까지 “사용자 플로우” 전체 검증
    • 통합 테스트: 여러 서버-side 구성 요소(프레임워크, 서비스, DB, 외부 연동 등)가 제대로 연동되는지 테스트
    • 단위 테스트: 한 클래스/메서드 단위의 동작을 대역을 이용해 빠르게 검증
  2. 범위가 넓을수록
    • 준비 비용커지, 실행 속도낮아짐
    • 하지만 실제 환경에 더 가깝게 검증 가능
    • 예외 상황/복잡한 조합은 오히려 단위 테스트+대역으로 더 잘 다룰 수 있음
  3. 테스트 전략
    • 다양한 경우의 수와 예외 상황은 단위 테스트에서 최대한 커버
    • 통합/기능(E2E) 테스트는 주요 플로우, 설정, 실제 연동 동작 위주
    • 그래야 전체 테스트 시간이 감당 가능한 수준으로 유지되고, TDD 피드백 루프도 빠르게 유지할 수 있다.
  4. 외부 연동 테스트 실전 예
    • 스프링 부트 + DB 통합 테스트
      • 실제 DB를 쓰되, 테스트 코드에서 insert/delete 로 상황을 제어
    • WireMock으로 REST 클라이언트 테스트
      • HTTP 서버 자체를 대역으로 띄워 응답/지연을 마음대로 조작
    • 스프링 부트 내장 서버 + TestRestTemplate
      • 실제 HTTP API 레벨에서 E2E 테스트를 JUnit 안에서 수행