728x90
반응형
"테스트가 없는 코드는 신뢰할 수 없다"
- Kent Beck -
실무에서 안정적인 백엔드 시스템을 개발하려면 "작동하는 코드"뿐만 아니라 "신뢰할 수 있는 테스트 코드"가 필수입니다. 이번 글에서는 Spring Boot 환경에서 JUnit5와 Mockito를 사용한 유닛 테스트의 모든 것을 실무 코드 예제와 함께 자세히 알아보겠습니다.
유닛 테스트란 무엇인가?
- 유닛(Unit)은 프로그램의 최소 구성 단위로, 일반적으로 하나의 클래스 또는 메서드를 의미합니다. 유닛 테스트(Unit Test)는 이 단위를 독립적으로 검증하는 테스트입니다.
핵심 목적: 외부 시스템(데이터베이스, 네트워크, API 등)과 분리된 상태에서 비즈니스 로직의 동작을 빠르게 검증하는 것
유닛 테스트와 통합 테스트의 구분
단일 컴포넌트(클래스/메서드)만 검증 | 여러 컴포넌트가 함께 동작하는 것을 검증 |
외부 의존성은 모두 Mock으로 대체 | 실제 데이터베이스나 외부 시스템과 연동 |
빠르게 실행되며 격리성이 높음 | 유닛 테스트보다 느리지만 실제 환경과 유사한 검증 가능 |
테스트 환경 설정: JUnit5 + Mockito
build.gradle 설정
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mockito:mockito-core'
}
spring-boot-starter-test에는 JUnit5, Mockito 등 테스트에 필요한 대부분의 도구가 포함되어 있습니다.
테스트 프레임워크 설명
JUnit5
- 정의: Java용 단위 테스트 프레임워크의 최신 버전
- 역할: 테스트 실행 환경 제공, 테스트 결과 검증(assert) 기능 제공
- 주요 기능: 테스트 라이프사이클 관리, 다양한 어노테이션 지원, 검증 메서드 제공
Mockito
- 정의: 자바 목(Mock) 객체 생성 프레임워크
- 역할: 실제 객체 대신 테스트용 가짜(Mock) 객체를 생성해 사용
- 사용 이유: 외부 의존성 분리로 테스트 대상만 순수하게 검증 가능
테스트 대상: 회원 가입 서비스
실제 예제를 통해 테스트 방법을 알아보겠습니다. 아래는 간단한 회원 가입 로직을 가진 서비스 클래스입니다.
@Service
public class MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
public MemberService(MemberRepository memberRepository,
PasswordEncoder passwordEncoder) {
this.memberRepository = memberRepository;
this.passwordEncoder = passwordEncoder;
}
public Long join(String email, String rawPassword) {
if (memberRepository.existsByEmail(email)) {
throw new IllegalArgumentException("이미 등록된 이메일입니다.");
}
String encodedPassword = passwordEncoder.encode(rawPassword);
Member member = new Member(email, encodedPassword);
return memberRepository.save(member).getId();
}
}
이 서비스는 두 가지 주요 기능을 합니다:
- 이메일 중복 체크
- 비밀번호 암호화 후 회원 정보 저장
테스트 코드 작성하기
테스트 클래스 기본 구조
@ExtendWith(MockitoExtension.class)
class MemberServiceTest {
@Mock
private MemberRepository memberRepository;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private MemberService memberService;
주요 어노테이션 설명
어노테이션 역할 효과
@ExtendWith(MockitoExtension.class) | JUnit5에서 Mockito 확장 기능 사용 | Mockito 기능을 JUnit5에서 원활하게 사용 가능 |
@Mock | 가짜(Mock) 객체 생성 | 실제 구현체 없이 메서드 동작 정의 가능 |
@InjectMocks | Mock 객체들을 테스트 대상에 주입 | 의존성 주입이 자동으로 이루어짐 |
테스트 케이스 작성
1. 회원 가입 성공 테스트
@Test
void join_성공_테스트() {
// given
String email = "test@example.com";
String rawPassword = "1234";
String encodedPassword = "encoded1234";
when(memberRepository.existsByEmail(email)).thenReturn(false);
when(passwordEncoder.encode(rawPassword)).thenReturn(encodedPassword);
when(memberRepository.save(any(Member.class)))
.thenReturn(new Member(1L, email, encodedPassword));
// when
Long result = memberService.join(email, rawPassword);
// then
assertThat(result).isEqualTo(1L);
verify(memberRepository).existsByEmail(email);
verify(passwordEncoder).encode(rawPassword);
verify(memberRepository).save(any(Member.class));
}
2. 중복 이메일 예외 테스트
@Test
void join_중복이메일_예외_테스트() {
// given
String email = "dup@example.com";
when(memberRepository.existsByEmail(email)).thenReturn(true);
// when & then
assertThrows(IllegalArgumentException.class, () -> {
memberService.join(email, "password");
});
verify(memberRepository).existsByEmail(email);
verify(memberRepository, never()).save(any());
}
Mockito 핵심 개념 상세 설명
Mock 객체와 스텁(Stub)
- Mock 객체: 실제 객체처럼 동작하지만 테스트를 위해 생성된 가짜 객체
- 스텁(Stub): when(...).thenReturn(...)으로 가짜 응답을 정의하는 것
Mockito 주요 메서드 설명
when().thenReturn()
- 역할: Mock 객체의 특정 메서드 호출에 대한 반환값 정의
- 예시: when(memberRepository.existsByEmail(email)).thenReturn(false);
- 의미: "memberRepository.existsByEmail(email)이 호출되면 false를 반환하라"
verify()
- 역할: 특정 Mock 객체 메서드의 호출 여부 검증
- 예시: verify(memberRepository).existsByEmail(email);
- 의미: "memberRepository.existsByEmail(email) 메서드가 정확히 1번 호출되었는지 확인"
never()
- 역할: 메서드가 호출되지 않았음을 검증
- 예시: verify(memberRepository, never()).save(any());
- 의미: "memberRepository.save() 메서드가 절대 호출되지 않았음을 확인"
any()
- 역할: 어떤 값이든 매칭되는 인자 매처(Argument Matcher)
- 예시: any(Member.class)
- 의미: "Member 타입의 어떤 객체든 매칭"
테스트 구조: Given-When-Then 패턴
테스트 코드의 가독성을 높이는 표준 패턴입니다.
단계 설명 예시 코드
Given(준비) | 테스트 데이터와 Mock 객체 동작 정의 | when(repository.existsByEmail(email)).thenReturn(false); |
When(실행) | 테스트 대상 메서드 실행 | Long result = memberService.join(email, rawPassword); |
Then(검증) | 실행 결과가 예상과 일치하는지 검증 | assertThat(result).isEqualTo(1L); verify(repository).save(any()); |
유닛 테스트의 실무적 가치
1. 디버깅 용이성
- 문제 발생 시 정확히 어느 부분이 잘못되었는지 빠르게 파악 가능
- 테스트 실패가 특정 유닛으로 격리되어 원인 추적이 쉬움
2. 개발 속도 향상
- 전체 애플리케이션을 실행하지 않고도 코드 동작 검증 가능
- CI/CD 파이프라인에서 빠른 피드백 제공
3. 리팩토링 안전망
- 기존 기능을 변경할 때 회귀 오류 방지
- 코드 변경의 부작용을 빠르게 확인 가능
4. 설계 개선
- 테스트하기 쉬운 코드는 대체로 더 나은 설계를 가짐
- 의존성 분리와 단일 책임 원칙을 자연스럽게 적용하게 됨
실무 팁: 좋은 테스트 코드를 위한 3가지 기준
기준 설명 실천 방법
격리성 | 테스트 대상 외 모든 의존성을 Mock으로 대체 | @Mock과 스텁을 적극 활용 |
가독성 | 테스트 코드의 의도를 명확히 전달 | Given-When-Then 구조 사용, 명확한 테스트 메서드 이름 |
신뢰성 | 일관된 테스트 결과 보장 | 외부 의존성 제거, 테스트 데이터 고정 |
유닛 테스트는 번거로워 보일 수 있지만, 실무에서 수많은 버그를 사전에 잡아내는 방어선이 됩니다. 특히 복잡한 로직일수록, 한번 작성된 테스트 코드는 가장 강력한 문서이자 방패가 됩니다.
JUnit5와 Mockito를 활용한 유닛 테스트는 Spring Boot 백엔드 개발에서 코드 품질을 보장하는 필수적인 도구입니다. 이 글에서 설명한 패턴과 기법을 실무에 적용하여 더 안정적이고 유지보수하기 쉬운 코드를 작성하시길 바랍니다.
반응형
'Java > Java' 카테고리의 다른 글
PreparedStatement 란? 기본개념, 장점, 성능향상, 메서드 활용과 주의사항 등 (1) | 2025.04.09 |
---|---|
Java 음성 소켓 통신(서버, 클라이언트) 기초 (0) | 2023.04.21 |
JAVA 채팅 프로그램 (0) | 2022.10.20 |
JAVA (TCP/IP, 서버 클라이언트) (0) | 2022.10.20 |
JAVA 네트워크 (0) | 2022.10.20 |