[테스트 회고] 로그인 Controller, Service, Cookie 테스트 책임 나누기
들어가며
오늘은 로그인 기능 주변 테스트를 정리했다.
로그인 기능은 단순히 이메일과 비밀번호를 받아 성공 응답을 내려주는 API처럼 보일 수 있다. 하지만 JWT 기반 인증 구조에서는 로그인 성공 이후에 access token, refresh token, cookie, validation, service 예외 흐름이 함께 움직인다.
그래서 테스트도 한 곳에 몰아넣기보다 계층별 책임을 나눠서 작성하는 것이 중요하다고 느꼈다.
이번 글은 UserControllerTest, UserServiceTest, JwtCookieProviderTest를 정리하면서 배운 내용을 기록한 글이다.
Controller 테스트에서 확인할 것
먼저 UserControllerTest의 로그인 성공 테스트를 정리했다.
기존에는 응답 body가 정상적으로 내려오는지 확인하는 데 집중했다. 하지만 실제 로그인 API에서는 쿠키도 중요한 응답이다.
로그인 성공 시 서버는 다음 흐름을 만든다.
1
2
3
4
5
6
7
로그인 요청
-> 사용자 인증 성공
-> accessToken 생성
-> refreshToken 생성
-> accessToken cookie 생성
-> refreshToken cookie 생성
-> 응답 body와 cookie 반환
따라서 Controller 테스트에서는 body뿐 아니라 cookie도 검증해야 한다고 판단했다.
확인한 내용은 다음과 같다.
- accessToken cookie 존재 여부
- refreshToken cookie 존재 여부
- cookie value
- HttpOnly 여부
- path
- maxAge
- 로그인 성공 응답 body
쿠키 기반 인증에서는 cookie 설정이 조금만 어긋나도 프론트엔드에서 인증 상태가 깨질 수 있다. 그래서 이 부분을 테스트에 포함하는 것이 좋다고 느꼈다.
validation은 Controller 테스트에서
로그인 validation 실패 케이스도 추가했다.
예를 들어 이메일 형식이 잘못된 요청은 Service까지 내려가기 전에 Controller 계층에서 걸러져야 한다.
@NotBlank, @Email 같은 DTO validation은 HTTP 요청과 직접 연결되어 있기 때문에 MockMvc 기반 Controller 테스트에서 검증하는 것이 자연스럽다.
정리하면 다음과 같다.
1
2
3
4
5
DTO validation 실패
-> Controller 테스트에서 검증
비즈니스 조건 실패
-> Service 테스트에서 검증
이 기준을 잡으니 테스트 위치를 정하는 것이 조금 쉬워졌다.
Service 테스트에서 확인할 것
다음으로 UserServiceTest를 보강했다.
Service 테스트에서는 HTTP 응답보다 비즈니스 흐름을 확인하는 것이 중요하다.
예를 들어 회원가입에서 중복 이메일이 발견되면 다음 단계로 진행하면 안 된다.
- 비밀번호 암호화가 실행되면 안 됨
- 사용자 저장이 실행되면 안 됨
그래서 never() 검증을 추가했다.
1
2
verify(passwordEncoder, never()).encode(anyString());
verify(userRepository, never()).save(any());
이 검증은 트랜잭션 롤백을 확인하는 테스트가 아니다.
실패 조건에서 불필요한 다음 동작으로 넘어가지 않는다는 것을 명시하는 테스트다.
로그인 실패 흐름
로그인 실패 흐름도 세분화해서 봤다.
존재하지 않는 이메일로 로그인하면 비밀번호 비교까지 갈 필요가 없다.
1
2
3
4
이메일 조회 실패
-> 예외 발생
-> passwordEncoder.matches() 호출 안 됨
-> JWT 생성 안 됨
비밀번호가 일치하지 않는 경우에는 사용자는 존재하지만 JWT가 생성되면 안 된다.
1
2
3
4
이메일 조회 성공
-> 비밀번호 불일치
-> 예외 발생
-> JWT 생성 안 됨
이렇게 실패 흐름을 나누어 테스트하니 로그인 로직이 더 명확하게 보였다.
로그인 성공 흐름
로그인 성공 시에는 JWT 생성 메서드가 올바른 값으로 호출되는지 확인했다.
단순히 토큰 문자열이 만들어졌는지만 보는 것이 아니라, 어떤 이메일과 권한으로 토큰을 생성하는지 확인하는 것이 중요했다.
로그인 성공 테스트에서 확인한 핵심은 다음이다.
- access token 생성 메서드 호출
- refresh token 생성 메서드 호출
- 올바른 이메일 전달
- 올바른 권한 전달
이 검증은 이후 JwtTokenProvider 테스트와 연결된다.
Service 테스트에서는 “생성을 요청했는가”를 확인하고, Provider 테스트에서는 “토큰이 제대로 생성되는가”를 확인하면 된다.
JwtCookieProviderTest 추가
쿠키 생성 로직은 별도 단위 테스트로 분리했다.
JwtCookieProvider는 Spring context 없이 테스트할 수 있는 비교적 단순한 컴포넌트다.
그래서 순수 단위 테스트로 access token cookie, refresh token cookie, 삭제용 cookie를 검증했다.
검증한 내용은 다음과 같다.
- access token cookie 이름
- refresh token cookie 이름
- cookie value
- 삭제 cookie의 value
- 삭제 cookie의
maxAge = 0
로그아웃에서 쿠키 삭제가 제대로 되려면 삭제용 cookie가 정확히 만들어져야 한다.
특히 같은 이름과 path로 maxAge를 0으로 내려줘야 브라우저에서 기존 쿠키를 제거할 수 있다.
테스트 책임을 나누기
오늘 정리하면서 가장 중요하다고 느낀 것은 테스트 책임을 나누는 것이다.
하나의 로그인 기능이라도 여러 계층으로 나누어 볼 수 있다.
| 테스트 | 책임 |
|---|---|
| ControllerTest | HTTP 요청/응답, validation, cookie 반환 |
| ServiceTest | 비즈니스 흐름, 실패 조건, 의존성 호출 여부 |
| CookieProviderTest | cookie 생성 규칙 |
| TokenProviderTest | JWT 생성과 claim 검증 |
이렇게 나누면 테스트가 더 읽기 쉬워진다.
또 실패했을 때 어느 계층에서 문제가 생겼는지도 더 빨리 찾을 수 있다.
다음 단계
다음 단계는 JwtTokenProviderTest를 작성하는 것이다.
JWT 테스트에서는 토큰 문자열 자체를 비교하기보다, 같은 secret으로 parsing해서 claim을 확인하는 방식이 좋다.
확인해야 할 내용은 다음과 같다.
- subject
- role claim
- type claim
- expiration
- access token과 refresh token의 차이
JWT는 인증의 핵심이기 때문에 생성만 확인하는 것보다 내부 claim이 의도대로 들어갔는지 확인해야 한다.
마무리
오늘은 로그인 기능의 테스트를 계층별로 정리했다.
처음에는 로그인 성공 여부만 보면 된다고 생각했지만, 실제로는 validation, service 실패 흐름, JWT 생성, cookie 생성까지 함께 봐야 했다.
테스트를 작성하면서 인증 흐름이 더 선명해졌다.
앞으로도 테스트를 작성할 때는 “무엇을 검증하는 테스트인가”를 먼저 정하고, 그 책임에 맞는 계층에서 검증하려고 한다.
Leave a comment