테스트코드 개념/ 종류/ 장점/ 주의사항/ TDD
테스트 코드란?
테스트 코드(Test Code)는 개발한 소프트웨어가 예상대로 동작하는지 자동으로 검증하는 코드입니다.
일반적으로 단위 테스트(Unit Test), 통합 테스트(Integration Test), 기능 테스트 (Functional Test, End-to-End Test, E2E)
등의 형태로 작성됩니다.
1. 단위 테스트 (Unit Test)
✔️ 정의:
- 프로그램의 개별 단위(함수, 메서드, 클래스)가 정상적으로 동작하는지 검증하는 테스트
- 보통 Mock 객체를 활용하여 다른 의존성을 제거하고 해당 단위만 테스트
✔️ 특징:
- 빠르고 독립적 (테스트 실행 속도가 빠름)
- 데이터베이스, 네트워크 같은 외부 의존성 없이 실행 (Mocking 활용)
- 버그를 조기에 발견할 수 있음
- 자바에서는 JUnit 프레임워크를 통해 단위테스트 코드를 작성할 수 있음
2. 통합 테스트 (Integration Test)
✔️ 정의:
- 여러 모듈 또는 컴포넌트가 올바르게 작동하는지 검증하는 테스트
- 단위 테스트와 달리, 데이터베이스, 외부 API, 네트워크 등의 실제 환경과의 연동을 포함
✔️ 특징:
- 실제 데이터베이스, API, 파일 시스템 등을 사용
- 여러 모듈이 서로 올바르게 연동되는지 확인
- 속도가 단위 테스트보다 느릴 수 있음
3. 기능 테스트 (Functional Test, End-to-End Test, E2E)
✔️ 정의:
- 애플리케이션의 전체 기능이 요구사항대로 동작하는지 검증하는 테스트
- 실제 사용자 흐름(로그인 → 상품 주문 → 결제 등)을 시뮬레이션
✔️ 특징:
- 실제 운영 환경과 유사한 환경에서 테스트 진행
- UI/UX까지 포함하여 사용자 경험을 테스트
- 실행 속도가 가장 느림
- 배포 전 최종 검증 단계에서 주로 사용
테스트 코드 작성 시 주의사항
- 단일 책임 원칙(Single Responsibility Principle) 준수
- 하나의 테스트는 하나의 기능만 검증해야 합니다.
- 여러 기능을 한 테스트에서 검증하면, 어디서 문제가 발생했는지 찾기 어렵습니다.
- 테스트 케이스의 독립성 유지
- 각 테스트는 서로 영향을 주지 않도록 독립적으로 실행 가능해야 합니다.
- 공유된 전역 상태를 변경하거나, 이전 테스트 결과에 의존하지 않도록 해야 합니다.
- AAA 패턴(Arrange, Act, Assert) 준수 (3A pattern)
- Arrange(준비): 필요한 데이터나 환경 설정
- Act(실행): 테스트할 기능 실행
- Assert(검증): 기대한 결과와 실제 결과 비교
저 또한 프로젝트 당시 작성한 단위테스트 코드에서 이러한 AAA패턴을 준수하도록 노력하였습니다.
이 함수는 예약 확정 실패 시, 이미 같은 시간과 방에 대해 확정된 예약이 있는 경우 발생하는 예외를 테스트하는 단위 테스트입니다.
void confirmReservation_Fail_AlreadyConfirmedSameTimeAndRoom() throws Exception {
// Arrange (준비)
Long id = 1L;
Long userId = 1L;
when(reservationService.confirmStatusReservation(any(), anyLong()))
.thenThrow(new AppException(RESERVATION_CONFIRMED_DUPLICATED_TIME_ROOM));
// Act (실행)
ResultActions result = mockMvc.perform(patch("/agents/reservations/{id}", id)
.with(SecurityMockMvcRequestPostProcessors.csrf()))
.andDo(print());
// Assert (검증)
result.andExpect(status().isConflict())
.andExpect(jsonPath("$.resultCode").value("ERROR"))
.andExpect(jsonPath("$.code").value(RESERVATION_CONFIRMED_DUPLICATED_TIME_ROOM.name()))
.andExpect(jsonPath("$.message").value(RESERVATION_CONFIRMED_DUPLICATED_TIME_ROOM.getMessage()));
}
@MockBean을 통해 HTTP 요청을 직접 Mocking하지 않고도 내부 로직을 쉽게 Mocking할 수 있다
일반적으로 HTTP Mocking을 하려면 실제 요청을 Mock 서버로 보내고 응답을 가짜로 설정하는 방식을 사용해야 합니다. 하지만 @MockBean을 활용하면, Spring 컨텍스트 내부에서 특정 빈의 동작을 직접 Mocking할 수 있으므로, 굳이 HTTP 레벨에서 Mocking할 필요 없이 더 간단하게 테스트가 가능합니다.
즉, "Mock Server를 만들 필요 없이 @MockBean을 이용해 직접 서비스 계층의 동작을 제어할 수 있다"는 의미입니다.
@MockBean
private ReservationService reservationService;
이렇게 Mocking을 하면 HTTP Mocking에 비해서 비교적 쉽게 여러 가지 테스트 케이스의 코드 작성이 가능하게 되며 최종적으로 폭넓은 테스트 케이스를 커버할 수 있게 됩니다.
테스트 코드 장점
1. 코드 품질 향상
2. 문서화
코드의 목적과 동작을 명확하게 설명하여, 개발자가 테스트를 쉽게 이해하고 유지보수할 수 있도록 돕는다는 점입니다. 즉, 테스트 코드 자체를 문서처럼 활용하여 테스트가 무엇을 검증하는지, 왜 필요한지, 어떤 조건에서 실행되는지 등을 설명하는 것입니다.
저는 @DisplayName 어노테이션을 사용해서 테스트코드의 가독성을 향상 시켰습니다.
@Test
@DisplayName("잘못된 요청으로 예약 가능 시간 조회 실패")
void getAvailableTimes_BadRequest() throws Exception {
// Given
Long roomId = 1L;
String invalidDate = "invalid-date";
// When & Then
mockMvc.perform(get("/reservations/available-times")
.param("roomId", String.valueOf(roomId))
.param("date", invalidDate)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
3. 리팩토링
리팩토링을 진행할 때, 기존 코드의 동작이 깨지지 않도록 확인하는 것이 매우 중요합니다. 테스트 코드는 리팩토링 후 기존 기능이 정상적으로 동작하는지 확인하는 안전망 역할을 합니다. 테스트가 잘 작성된 경우, 리팩토링 후 기존 기능의 동작이 변경되지 않았는지 자동으로 검증할 수 있습니다.
@WebMvcTest
Spring MVC의 웹 계층을 테스트할 때 사용하는 JUnit 테스트 어노테이션입니다. 이 어노테이션은 컨트롤러와 관련된 테스트를 수행하는 데 유용합니다. @WebMvcTest를 사용하면 웹 계층만 테스트하며, 서비스나 리포지토리 등 다른 계층은 실제로 실행되지 않도록 하여, 빠르고 효율적인 웹 계층 테스트가 가능하게 됩니다.
@WebMvcTest(controllers = AgentReservationController.class,
excludeAutoConfiguration = SecurityAutoConfiguration.class,
excludeFilters =
{@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {OncePerRequestFilter.class})})
TDD란?
TDD(Test-Driven Development)는 테스트 주도 개발이라고도 하며, 테스트를 먼저 작성하고 그 테스트를 통과할 수 있는 코드를 작성하는 개발 방법론입니다.
TDD의 기본 흐름 (Red-Green-Refactor)
- Red - 테스트 작성: 먼저 원하는 기능에 대한 테스트 코드를 작성합니다. 이 테스트는 기능이 구현되지 않았기 때문에 당연히 실패합니다.
- Green - 코드 구현: 테스트가 실패하는 것을 확인한 후, 해당 테스트를 통과할 수 있는 최소한의 코드를 작성하여 테스트를 통과하도록 합니다.
- Refactor - 리팩토링: 테스트가 통과한 후, 작성한 코드를 리팩토링하여 더 깔끔하고 효율적으로 만듭니다. 리팩토링 후에도 테스트는 계속 통과해야 합니다.
저 또한 프로젝트에서 TDD 방식을 통해 프로젝트를 진행했으며, 작성된 코드는 자동화된 테스트에 의해 지속적으로 검증되므로 버그 발생 가능성이 확연히 적음을 몸소 느낄 수 있었습니다. TDD 개발자이자 미국의 소프트웨어 엔지니어 켄트 벡은 TDD를 이용한 소프트웨어 개발이란 코드변경에 대한 두려움에서 지루함으로 바뀌는 과정이라고 이야기했습니다. 실제로 저를 포함한 팀원들은 코드를 작성하는 것보다 때로는 테스트 코드 작성이 더 오래걸리기도하며 지루함을 느끼기도 하였습니다. 하지만 그 지루함 뒤에는, 생각한 대로 코드가 동작한다는 확신을 얻을 수 있었습니다.
다음 프로젝트를 할 때에는 unit테스트에서 더 나아가 통합, E2E 테스트를 통해서 더 유지보수가 용이하고 품질 높은 코드를 작성해보고 싶습니다.
참고자료
GitHub - Teammyong/Ownbang: 화상 통화를 이용한 비대면 부동산 중개 서비스
화상 통화를 이용한 비대면 부동산 중개 서비스. Contribute to Teammyong/Ownbang development by creating an account on GitHub.
github.com
실무에서 적용하는 테스트 코드 작성 방법과 노하우 Part 1: 효율적인 Mock Test
실무에서 적용하는 테스트 코드 작성 방법과 노하우 Part 1: 효율적인 Mock Test | 카카오페이 기술
Mock 테스트 코드 작성 중에 마주한 문제들과 그 문제를 해결하는 방법과 노하우를 소개드립니다.
tech.kakaopay.com
실무에서 적용하는 테스트 코드 작성 방법과 노하우 Part 3: Given 지옥에서 벗어나기 - 객체 기반 데이터 셋업의 한계
실무에서 적용하는 테스트 코드 작성 방법과 노하우 Part 3: Given 지옥에서 벗어나기 - 객체 기반
Mock 테스트 코드 작성 중에 마주한 문제들과 그 문제를 해결하는 방법과 노하우를 소개드립니다.
tech.kakaopay.com