단위 테스트와 GWT 패턴
1. 기존
1. `Application.java`의 재생 버튼(▶)을 눌러 스프링 부트 서버를 켠다.
2. 포스트맨(Postman)을 열고 JSON 바디를 한 땀 한 땀 작성한다.
3. `Send` 버튼을 누르고 응답을 확인한다.
4. "어라? 에러 나네?" 코드를 수정하고 1번부터 다시 시작한다.
2. 수동 테스트 의존하면 안 되는 이유
- 회귀 버그: 한 코드를 수정하며 공통 모듈을 건드리는 바람에 다른 코드에서 버그가 생김 > postman을 이용하면 일일이 다시 실행해서 확인해봐야 함
- 느린 피드백 루프: 스프링 부트가 켜지는데 오래 걸림 / 에러 하나 고치고 확인하는데 오래 걸림
- 문서화 부재: 남의 코드를 보고 수정해야 하는데 확인하기 어려움
3. 단위 테스트
1) 개념
- 개발자의 의도대로 정확히 작동하는지 코드로 코드를 검증하는 자동화된 과정
- 함수 단위로 테스트
2) Assertion (단언 / 검증)
- Assertion: 내가 예상한 결과(Expected)와 실제 실행된 결과(Actual)가 일치하는지 단호하게 따져 묻는 것
- 일반 코드: int result = calculator.add(1, 2); (실행하고 끝)
- 테스트 코드 (Assertion): assertThat(result).isEqualTo(3); (결과가 3이 아니면 테스트 실패 처리!)
- 주로 AssertJ라는 라이브러리의 assertThat() 메서드를 사용
3) Given-When-Then (GWT) 패턴
- Given (준비)
- 테스트를 실행하기 위한 '환경과 데이터'를 세팅하는 단계입니다.
- "어떤 상황이 주어졌을 때"
- 예: "홍길동이라는 유저 객체를 만들고, 나이를 20살로 세팅한다."
- When (실행)
- 우리가 실제로 테스트하고자 하는 그 '메서드'를 호출하는 단계입니다.
- "그 행동을 실행하면"
- 예: "유저의 성인 여부를 확인하는 isAdult() 메서드를 호출한다."
- Then (검증)
- When 단계에서 나온 결과값을 앞서 배운 Assertion을 통해 검증하는 단계입니다.
- "이러한 결과가 나와야만 한다!"
- 예: "반환된 결과값이 true인지 검증한다. (assertThat(result).isTrue())"
Mockito를 활용한 AuthService 테스트
1. 서비스(Service)는 리포지토리(Repository)가 필요한 이유
- AuthService의 login() 메서드가 작동하려면 데이터베이스에서 유저 정보를 꺼내와야 함
- MemberRepository에 강하게 의존
- 진짜 DB 붙이면 테스트 속도 저하, 데이터 오염
2. Mocito
1) 역할
- DB 연결 없이, 마치 DB가 있는 것처럼 AuthService만 테스트
- 주연 배우 (AuthService): 우리가 진짜로 연기력(로직)을 평가하고 싶은 테스트의 주인공입니다
- 위험한 액션 씬 (DB 조회): 시간도 오래 걸리고 데이터 오염의 위험이 있는 작업입니다
- 대역 배우 (Mock Repository): 주연 배우를 대신해 액션 씬을 처리해 주는 '가짜 객체'입니다
2) Mockito의 3대장 용어
- @Mock (가짜 객체 생성)
- @InjectMocks (가짜 객체 주입)
- Stubbing (스텁 / 훈련시키기) : given() 또는 when().thenReturn() 이라는 메서드로 훈련
3. TDD : 실패하는 테스트를 먼저 작성
1) 로그인 실패 시나리오
- 이메일이 없는 경우 예외가 터져야 한다!
- 비밀번호가 틀린 경우 예외가 터져야 한다!
2) 실행
- 실제 로직이 없으니 테스트는 새빨간 에러(RED)를 뿜어내며 실패
- 빨간불을 초록불(GREEN)로 바꾸기 위해 다음 스텝에서 실제 서비스 로직을 채워 넣을 것
3) 실습
// Mockito 대역 배우들을 사용하겠다고 선언합니다!
@ExtendWith(MockitoExtension.class)
class AuthServiceTest {
// 1. 대역 배우 캐스팅
@Mock
private MemberRepository memberRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private JwtTokenProvider jwtTokenProvider;
// 2. 주연 배우에게 대역 배우들 주입
@InjectMocks
private AuthService authService;
@Test
@DisplayName("로그인 실패 - 존재하지 않는 회원")
void login_fail_user_not_found() {
// given: 상황 세팅 (존재하지 않는 유저네임으로 로그인 시도)
LoginRequest request = new LoginRequest("not_found_user", "password123!");
// 핵심! 대역 배우 훈련(Stubbing)시키기
// "누가 memberRepository.findByUsername()을 호출하든, 비어있는 Optional을 리턴해라!"
// Stubbing: 실제DB에 간 척
given(memberRepository.findByUsername(anyString())).willReturn(Optional.empty());
// when & then: authService.login()을 실행했을 때, MemberException이 "터져야만" 테스트 통과!
assertThatThrownBy(() -> authService.login(request))
.isInstanceOf(MemberException.class)
.hasMessage(MemberErrorCode.INVALID_CREDENTIALS.getMessage());
}
TDD, 로그인 API 서비스 로직 완성
1. 실제 개발자의 TDD 논리 프로세스
- "첫 번째 테스트(존재하지 않는 회원)를 통과시켜야 해."
- "두 번째 테스트(비밀번호 불일치)를 통과시켜야 해."
- "마지막, 진짜 로그인 성공 로직 완성하기."
2. TDD 실습
1) 존재하지 않는 회원 거르기
더보기
더보기
더보기
- 테스트 코드 작성 (Red)
// AuthServiceTest.java 내부에 추가
@Test
@DisplayName("로그인 실패 - 존재하지 않는 회원 ")
void login_fail_user_not_found() {
// given: 없는 유저네임으로 요청
LoginRequest request = new LoginRequest("not_found_user", "password123!");
// 대역 배우 훈련: "누가 조회하든 무조건 비어있는 Optional을 뱉어라!"
given(memberRepository.findByUsernameOrEmailOrPhone(anyString(), anyString(), anyString())).willReturn(Optional.empty());
// when & then: 예외가 터져야 성공! 예외 코드도 우리가 지정한 에러코드인지 확인
assertThatThrownBy(() -> authService.login(request))
.isInstanceOf(MemberException.class)
.hasMessage(MemberErrorCode.INVALID_CREDENTIALS.getMessage());
}
- 서비스 로직 구현(Green)
// AuthService.java 내부에 추가
@Transactional
public AuthTokens login(LoginRequest request) {
// 요구사항: username, email, phone 셋 중 하나를 선택해 로그인
String loginId = request.username();
// 1. 유저 조회 (없으면 예외를 뻥! 던집니다. 이걸로 Step 1 테스트가 통과됩니다!)
Member member = memberRepository.findByUsernameOrEmailOrPhone(loginId, loginId, loginId)
.orElseThrow(() -> new MemberException(MemberErrorCode.INVALID_CREDENTIALS));
return null; // 일단 컴파일을 위해 null 리턴
}
2) 비밀번호 불일치 거르기
더보기
더보기
더보기
- 테스트 코드 작성 (Red)
// AuthServiceTest.java 내부에 추가
@Test
@DisplayName("로그인 실패 - 비밀번호 불일치 시 없는 회원과 동일한 예외 반환")
void login_fail_invalid_password() {
// given: DB에 유저는 있지만, 입력한 비밀번호가 틀림
LoginRequest request = new LoginRequest("test_user", "wrong_password");
Member mockMember = Member.builder().username("test_user").password("encoded_correct_password").build();
ReflectionTestUtils.setField(mockMember, "id", 1L);
// 대역 배우 훈련 1: 유저 조회는 성공하게 만듦
given(memberRepository.findByUsernameOrEmailOrPhone(
eq(request.username()), eq(request.username()), eq(request.username())))
.willReturn(Optional.of(mockMember));
// 대역 배우 훈련 2: 비밀번호 검사기(PasswordEncoder)는 무조건 false를 뱉게 만듦
given(passwordEncoder.matches(eq(request.password()), eq(mockMember.getPassword())))
.willReturn(false);
// when & then: 예외가 터지는지 확인
assertThatThrownBy(() -> authService.login(request))
.isInstanceOf(MemberException.class)
.hasMessage(MemberErrorCode.INVALID_CREDENTIALS.getMessage());
}
- 서비스 로직 구현(Green)
// AuthService.java 의 login 메서드에 이어서 작성
// 2. 비밀번호 불일치 시 예외 던지기 (이걸로 Step 3 테스트가 통과됩니다!)
if (!passwordEncoder.matches(request.password(), member.getPassword())) {
throw new MemberException(MemberErrorCode.INVALID_CREDENTIALS);
}
3) 로그인 성공(토큰 발급)
더보기
더보기
더보기
- 테스트 코드 작성 (Red)
// AuthServiceTest.java 내부에 추가
@Test
@DisplayName("로그인 성공 - 토큰 페어 정상 발급")
void login_success() {
// given: 모든 것이 완벽한 상황 세팅
LoginRequest request = new LoginRequest("test_user", "correct_password");
/* ... mockMember 생성 및 repository, passwordEncoder Stubbing (위와 동일하여 생략) ... */
given(passwordEncoder.matches(eq(request.password()), eq(mockMember.getPassword())))
.willReturn(true);
// 대역 배우 훈련 3: 토큰 발급기(JwtTokenProvider)가 가짜 토큰을 뱉도록 훈련
given(jwtTokenProvider.createAccessToken(eq(1L), anyString())).willReturn("mock.access.token");
given(jwtTokenProvider.createRefreshToken(eq(1L))).willReturn("mock.refresh.token");
// when
AuthTokens tokens = authService.login(request);
// then: 토큰이 잘 받아와졌는지 검증
assertThat(tokens).isNotNull();
assertThat(tokens.accessToken()).isEqualTo("mock.access.token");
assertThat(tokens.refreshToken()).isEqualTo("mock.refresh.token");
// 핵심! 행동 검증 (Behavior Verification)
// 토큰 발급기의 메서드가 "정말로 1번씩 호출되었는가?"를 따져 묻는 현업 필수 검증 기법입니다!
then(jwtTokenProvider).should().createAccessToken(eq(1L), anyString());
then(jwtTokenProvider).should().createRefreshToken(eq(1L));
}
- 서비스 로직 구현 (Green)
// AuthService.java 의 login 메서드에 이어서 작성
// 3. 토큰 발급 및 DB 저장 (이걸로 대망의 성공 테스트가 통과됩니다!)
String accessToken = jwtTokenProvider.createAccessToken(member.getId(), member.getRole().name());
String refreshToken = jwtTokenProvider.createRefreshToken(member.getId());
// Refresh Token DB 저장 로직 수행
member.updateRefreshToken(refreshToken);
return new AuthTokens(accessToken, refreshToken);
3. TDD & Mockito 심화
1) assertThat과 then().should() 다른 점
- assertThat (상태 검증 / 결과값 검증): 최종 결과물(데이터)을 평가하는 것 / tokens.accessToken()이 "mock..."과 일치하는지
- then().should() (행동 검증): 내부 메서드가 호출된 횟수와 과정을 감시하는 것
2) TDD는 Red -> Green -> Refactor / Refactor 단계에선 하는 것
- 통과시킨 코드를 '아름답고 객체지향적으로 다듬는 과정
3) Mock 통과했는데 실제 DB에서 터짐
- 통합 테스트(Integration Test, @SpringBootTest)를 추가로 작성
- 사용자의 API 요청부터 DB 저장까지의 '전체 흐름(Full-Cycle)'을 한 번 더 검증
- 단위 테스트: 빠르고 가벼움 (전체 테스트의 70~80% 비중)
- 통합 테스트: 느리고 무거움, 하지만 실제 환경과 가장 유사함 (20~30% 비중)
4) when 대신 given 쓰는 이유 (BDDMockito) : 가독성
[Refactor] 성능과 프론트엔드 친화성 둘 다 잡기
'🔌 SPARTA > Courses' 카테고리의 다른 글
| [클라우드 기반 백엔드 설계] 클라우드와 AWS 기초 (0) | 2026.03.10 |
|---|---|
| 스탠다드반 10회차: Filters & Proxy Objects (0) | 2026.03.09 |
| 스탠다드반 8회차 : 로깅(AOP)와 인증/인가의 시작(JWT) (0) | 2026.03.04 |
| Spring Security와 SecurityContext (0) | 2026.03.03 |
| JWT와 Filter (0) | 2026.03.03 |