시작하며...
항상 프로젝트와 과제를 수행함에 있어서 테스트 코드 작성에 익숙하지 않고, 어떻게 접근해야 하는지에 대한 궁금증만 가지고 교육과정을 진행하고 있었다.
오늘은 테스트코드의 필요성과 개념, 그리고 앞으로 프로젝트와 과제를 진행함에 있어서 꾸준히 활용해야 할 내용에 대해서 정리해보고자 한다.
테스트코드란?
: 작성한 코드를 실행했을 때 에러가 나거나, 의도한 대로 동작하지 않는 경우를 방어하기 위한 검증의 과정이라고 할 수 있다.
테스트 코드를 작성해야 하는 핵심적인 이유
- 안정성 확보 (Stability)
- 새로운 기능을 추가하거나 기존 코드를 수정할 때, 기존 기능이 망가지지 않았는지(Regression) 즉시 확인할 수 있음
- 수정으로 인한 추가적인 에러나 예외발생에 대한 두려움을 없앨 수 있음.
- 빠른 피드백 (Fast Feedback)
- 서버를 띄우고, Postman으로 요청을 보내고, 로그를 확인하는 과정은 시간이 오래 걸림
- 테스트 코드는 버튼 하나로 수 초 이내의 로직 정합성 검증을 진행해줌
- 문서화 (Documentation)
- 테스트 코드는 그 자체로 살아있는 문서
- 요청과 응답의 과정에 대한 의문이 있을 때 테스트 코드를 보면 정확한 예시를 알 수 있음
- 리팩토링의 자신감
- 코드를 개선하고 싶을 때, 테스트가 있으면 기존 동작이 유지되는지 확인할 수 있음
- 리팩토링 후 모든 테스트가 통과하면, 안심하고 변경사항을 커밋할 수 있음
테스트 코드 작성 시와 미작성 시 비교
| 항목 | 테스트 코드 작성 | 테스트 코드 미작성 |
| 초기 개발 비용 | 개발 시간과 비용 증가 | 빠르게 개발 가능 |
| 버그 수정 비용 | 초기에 발견된 버그를 빠르고 저렴하게 해결 가능 | 배포 후 발견 시 높은 비용 발생 |
| 품질 | 코드 변경 시 안정성을 보장 | 코드 변경 시 회귀 오류 발생 가능성 높음 |
| 유지보수 | 기능 문서화 및 코드 이해도 증가 | 신규 팀원이 코드 이해 및 적응하는데 시간 소요 |
| 배포 안정성 | 자동화 테스트로 배포 전 품질 보장 | 배포 후 오류 발견 시 롤백 및 긴급 수정 필요 |
| 장애 발생 확률 | 회귀 테스트를 통해 장애 가능성 최소화 | 장애 발생 가능성 증가 |
테스트의 종류
단위 테스트(Unit Test)
- 작은 코드 단위를 독립적으로 검증하는 테스트
- 소프트웨어의 가장 작은 단위인 모듈, 함수, 클래스 등의 개별적인 기능을 테스트하는 것
- 주로 프로그래머가 작성하며, 코드의 동작을 검증하고 예상대로 동작하는지 확인한다.
- 코드 변경 시에 기존 코드의 충돌 및 오류를 발견 가능하며, 새로운 코드 동작 성공여부를 확인할 수 있다.
- TDD에서의 테스트 케이스는 주로 단위 테스트 작성을 의미한다. 주로 자동화되어 사용된다 (CI)
- 장점
- 실행 속도가 빠름
- 특정 로직만 집중해서 테스트 가능
- 외부 환경(DB, 네트워크)에 영향을 받지 않음
- 단점
- 실제 통합 환경에서 발생할 수 있는 문제를 발견하지 못할 수 있음
- Mock 설정이 복잡할 수 있음
통합 테스트(Integration Test)
- 단위 테스트에서 독립적인 기능을 확인했다면, 통합 테스트에서는 모듈 간의 연계된 동작을 확인한다.
- 각각의 모듈이 제대로 상호작용하는지, 시스템의 구성 요소들이 올바르게 통합되어 동작하는지를 확인한다.
- 장점
- 실제 환경과 유사한 상황에서 테스트
- 모듈 간 통합 문제를 발견할 수 있음
- End-to-End 시나리오 검증 가능
- 단점
- 실제 환경과 유사한 상황에서 테스트
- 모듈 간 통합 문제를 발견할 수 있음
- 테스트 간 데이터 격리가 필요
부하 테스트(Stress Test)
- 소프트웨어가 예상되는 최대 부하를 견딜 수 있는지를 검증한다.
- 시스템에 과도한 부하를 가하여 응답 시간, 처리량, 자원 사용량 등을 측정하고, 성능 문제를 파악 및 최적화할 수 있다.
언제 무엇을 사용해야 하는가?
| 테스트 대상 | 추천하는 테스트 타입 | 이유 |
| Repository | @DataJpaTest (통합) | JPA 쿼리 동작은 실제 DB로 확인 |
| Service | 단위 테스트 (@ExtendWith(MockitoExtension.class) |
비즈니스 로직만 집중 테스트 |
| Controller | @WebMvcTest (단위) | HTTP 요청/응답 처리만 테스트 |
| 전체 플로우 | @SpringBootTest (통합) | API 호출부터 DB 저장까지 전체 검증 |
| 일반 유틸 클래스 | 단위 테스트 | 의존성 없이 로직만 테스트 |
테스트 도구
: 프로젝트 내 기본 구성으로 테스트를 위한 도구들이 포함되어 있음
JUnit 5
자바 진영의 표준 테스트 프레임워크
주요 어노테이션
@Test
테스트 메서드임을 나타냄
@Test
void createBoard() {
// 이 메서드가 테스트로 실행됩니다
}
@BeforeEach
각 테스트 메서드 실행 전에 실행되며, 테스트 데이터를 초기화할 때 사용
@BeforeEach
void setUp() {
// 모든 테스트 전에 실행됨
boardRepository.deleteAll(); userRepository.deleteAll();}
@AfterEach
각 테스트 메서드 실행 후에 실행되며, 테스트 후 정리 작업에 사용
@AfterEach
void tearDown() {
// 모든 테스트 후에 실행됨
boardRepository.deleteAll();}
@BeforeAll / @AfterAll
모든 테스트 실행 전/후에 한번만 실행되며, static 메서드여야 한다.
@BeforeAll
static void setUpAll() {
// 모든 테스트 시작 전에 한 번만 실행
}
@DisplayName
테스트에 설명을 추가한다.
@Test
@DisplayName("게시글 생성 시 제목, 내용, 작성자가 올바르게 저장된다")
void createBoard() {
// ...}
AssertJ
테스트 검증을 위한 라이브러리이며 직관적인 문법을 제공한다.
기본 검증
// 값 비교
assertThat(actual).isEqualTo(expected);
// null 검증
assertThat(object).isNotNull();
assertThat(object).isNull();
// boolean 검증
assertThat(result).isTrue();
assertThat(result).isFalse();
// 문자열 검증
assertThat(str).contains("테스트");
assertThat(str).startsWith("Bearer ");
assertThat(str).isNotEmpty();
// 숫자 검증
assertThat(number).isGreaterThan(0);
assertThat(number).isLessThanOrEqualTo(10);
// 컬렉션 검증
assertThat(list).hasSize(2);
assertThat(list).isEmpty();
assertThat(list).contains(item1, item2);
예외 검증
// 예외가 발생하는지 검증
assertThatThrownBy(() -> boardService.getBoard(999L))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Board not found with id: 999");
Mockito
가짜 객체(Mock)를 쉽게 만들고 관리해주는 라이브러리이며, 단위 테스트의 필수요소이다.
주요 어노테이션
@ExtendWith(MockitoExtension.class)
- 클래스 레벨에 선언
- Mockito 기능을 활성화
@ExtendWith(MockitoExtension.class)
class BoardServiceTest {
// Mockito 사용 가능
}
@Mock
- 가짜 객체를 생성
- 실제 구현이 없는 껍데기만 있는 객체
@Mock
private BoardRepository boardRepository; // 가짜 Repository
@InjectMocks
- @Mock으로 만든 객체들을 자동으로 주입받음
- 테스트할 실제 객체를 생성
@InjectMocks
private BoardService boardService; // Mock Repository가 주입된 실제 Service
Mock 동작 정의 (Stubbing)
given().willReturn()
- Mock 객체가 어떻게 행동해야 할지 미리 정해준다.
// "boardRepository.save()가 호출되면 board를 반환해라"
given(boardRepository.save(any(Board.class))).willReturn(board);
// "boardRepository.findById(1L)이 호출되면 Optional.of(board)를 반환해라"
given(boardRepository.findById(1L)).willReturn(Optional.of(board));
// "boardRepository.findById(999L)이 호출되면 빈 Optional을 반환해라"
given(boardRepository.findById(999L)).willReturn(Optional.empty());
any()매처
- 어떤 값이든 상관없이 매칭
given(boardRepository.save(any(Board.class))).willReturn(board);
// save에 어떤 Board 객체가 들어와도 board를 반환
Mock 호출 검증
verify()
- Mock 객체의 메서드가 호출되었는지 확인
// deleteById가 정확히 한 번 호출되었는지 확인
verify(boardRepository).deleteById(boardId);
// save가 한 번도 호출되지 않았는지 확인
verify(boardRepository, never()).save(any());
// findAll이 정확히 2번 호출되었는지 확인
verify(boardRepository, times(2)).findAll();
Spring Boot Test 어노테이션
@SpringBootTest
전체 Spring 컨텍스트를 로드하며, 통합 테스트에 사용됨
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class BoardIntegrationTest {
// 실제 Spring 애플리케이션이 구동됨
// 모든 Bean이 로드됨
}
옵션 :
- RANDOM_PORT : 랜덤 포트로 서버를 띄움
- DEFINED_PORT : application.yml에 정의된 포트 사용
- MOCK : 서버를 띄우지 않고 MockMvc만 사용
@DataJpaTest
JPA 관련 컴포넌트만 로드, Repository 테스트에 최적화되어 있음
@DataJpaTest
@ActiveProfiles("test")
class BoardRepositoryTest {
// JPA Repository, EntityManager 등만 로드
// 훨씬 빠르게 실행됨
}
특징 :
- 자동으로 H2 인메모리 DB 사용
- 각 테스트마다 트랜잭션 롤백
- @Transactional이 기본 적용됨
@WebMvcTest
Controller 레이어만 테스트하며 Service는 Mock으로 대체한다.
@WebMvcTest(controllers = BoardController.class)
@Import(SecurityConfig.class) // Security 설정이 필요하면 importclass BoardControllerTest {
@Autowired
private MockMvc mockMvc; // HTTP 요청을 테스트할 수 있는 객체
@MockBean // Spring Context에 Mock Bean 등록
private BoardService boardService;
}
@MockBean vs @Mock :
- @MockBean : Spring Context에 Mock 객체를 등록 (Spring 테스트에서 사용)
- @Mock : 일반 Mock 객체 생성 (Mockito 단위 테스트에서 사용)
@ActiveProfiles("test")
테스트용 프로필을 활성화한다. src/test/resources/application.yml을 사용
@SpringBootTest
@ActiveProfiles("test") // H2 DB를 사용하도록 설정
class BoardIntegrationTest {
// test 프로필 활성화
}
테스트코드 작성 패턴 (Given-When-Then)
테스트코드는 보통 3단계로 구성되며, 패턴을 준수할 경우 가독성이 좋아진다.
Given (준비)
테스트에 필요한 데이터나 상황을 세팅
// given
BoardRequestDto requestDto = new BoardRequestDto("제목", "내용", "작성자");
Board board = Board.create("제목", "내용", "작성자");
given(boardRepository.save(any(Board.class))).willReturn(board);
When (실행)
실제로 테스트할 메서드를 호출
// when
BoardResponseDto responseDto = boardService.createBoard(requestDto);
Then (검증)
실행 결과가 기대한 값과 같은지 확인
// then
assertThat(responseDto.getTitle()).isEqualTo("제목");
assertThat(responseDto.getContent()).isEqualTo("내용");
assertThat(responseDto.getAuthor()).isEqualTo("작성자");
전체 예시
@Test
void createBoard() {
// given (준비): 테스트 데이터 준비
BoardRequestDto requestDto = new BoardRequestDto("제목", "내용", "작성자");
Board board = Board.create("제목", "내용", "작성자");
given(boardRepository.save(any(Board.class))).willReturn(board);
// when (실행): 테스트할 메서드 실행
BoardResponseDto responseDto = boardService.createBoard(requestDto);
// then (검증): 결과 검증
assertThat(responseDto.getTitle()).isEqualTo("제목");
assertThat(responseDto.getContent()).isEqualTo("내용");
}
💡 예외를 검증하는 경우 When과 Then을 합칠 수 있음
@Test
void updateBoard_NotFound() {
// given
Long boardId = 999L;
given(boardRepository.findById(boardId)).willReturn(Optional.empty());
// when & then
assertThatThrownBy(() -> boardService.updateBoard(boardId, requestDto))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Board not found with id: " + boardId);}'JAVA > Java 실습' 카테고리의 다른 글
| [Java] 테스트 코드 개념 정리하기 2 (0) | 2025.12.11 |
|---|---|
| [자바 알고리즘] 프로그래머스 두 개 뽑아서 더하기 (0) | 2025.11.10 |
| [JAVA Algorithm] 커머스 과제 알고리즘 기능 구현 (0) | 2025.10.13 |
| [Java Algorithm] 거스름돈 문제 풀이 예시 따라가보기 (0) | 2025.09.29 |
| [JAVA 실습] 커머스 과제 (심화) (0) | 2025.09.24 |