[테스트 회고] 로그인 응답 쿠키와 JWT 테스트를 보강하며 배운 점
들어가며
오늘은 로그인 기능 주변 테스트를 보강했다.
처음에는 로그인 성공 응답이 정상적으로 내려오는지만 확인하면 충분하다고 생각할 수 있다. 하지만 JWT 기반 로그인에서는 응답 body만큼이나 쿠키가 중요하다.
Access Token과 Refresh Token이 어떤 이름으로 내려오는지, HttpOnly 설정이 되어 있는지, path와 maxAge가 의도대로 잡히는지 확인하지 않으면 프론트엔드 연동이나 보안 흐름에서 문제가 생길 수 있다.
그래서 오늘은 Controller, Service, Cookie Provider, Token Provider 테스트의 책임을 나누어 정리해보았다.
UserControllerTest 보강
먼저 UserControllerTest의 로그인 성공 테스트를 보강했다.
기존에는 로그인 성공 시 응답 body 위주로 검증했다면, 이번에는 accessToken, refreshToken 쿠키까지 확인하도록 범위를 넓혔다.
컨트롤러 테스트에서 확인한 내용은 다음과 같다.
- 로그인 성공 응답 body
accessToken쿠키 존재 여부refreshToken쿠키 존재 여부- 쿠키 값
- HttpOnly 여부
- path
- maxAge
쿠키는 클라이언트와 서버 사이의 계약이다.
특히 Refresh Token을 HttpOnly Cookie로 내려주는 구조라면, 쿠키가 정상적으로 생성되는지 테스트하는 것이 중요하다.
validation 실패 케이스
로그인 validation 실패 케이스도 추가했다.
예를 들어 이메일 형식이 잘못된 요청은 서비스 로직까지 내려가기 전에 Controller 계층에서 막히는 것이 자연스럽다.
이때 @NotBlank, @Email 같은 DTO validation은 MockMvc 기반 컨트롤러 테스트에서 확인하는 것이 좋다고 정리했다.
반면 비밀번호 불일치, 존재하지 않는 이메일, 중복 이메일 같은 비즈니스 예외는 Service 테스트에서 검증하는 것이 더 자연스럽다.
정리하면 다음 기준이다.
| 테스트 대상 | 검증 내용 |
|---|---|
| ControllerTest | 요청 형식, validation, HTTP status, 응답 body, cookie |
| ServiceTest | 비즈니스 조건, 예외 흐름, Repository/Provider 호출 여부 |
| ProviderTest | JWT, Cookie 같은 순수 생성 로직 |
이 기준을 잡으니 테스트를 어디에 작성해야 할지 조금 더 명확해졌다.
UserServiceTest 보강
UserServiceTest에서는 실패 흐름을 더 자세히 검증했다.
특히 실패 조건에서 다음 단계가 실행되지 않는지 never()로 확인했다.
예를 들어 중복 이메일 회원가입 실패라면 비밀번호 암호화나 저장이 실행되면 안 된다.
1
2
verify(passwordEncoder, never()).encode(anyString());
verify(userRepository, never()).save(any());
존재하지 않는 이메일로 로그인할 때도 마찬가지다.
사용자가 없으면 비밀번호 비교까지 갈 필요가 없다.
따라서 passwordEncoder.matches()와 JWT 생성 메서드가 호출되지 않는지 확인했다.
이 never() 검증은 트랜잭션 롤백을 검증하는 것이 아니다.
실패 조건에서 불필요한 다음 단계로 진행하지 않는 흐름을 명시하는 용도에 가깝다.
로그인 성공 시 JWT 생성 검증
로그인 성공 케이스에서는 JWT 생성 메서드가 올바른 값으로 호출되는지 확인했다.
로그인에 성공하면 access token과 refresh token이 생성되어야 한다.
이때 이메일과 권한 정보가 의도대로 전달되는지 검증하는 것이 중요하다.
단순히 “로그인 성공”만 보는 것이 아니라, 로그인 성공 이후 인증 흐름에 필요한 값이 만들어지는지 확인해야 한다.
이 테스트는 이후 쿠키 생성 테스트와 연결된다.
1
2
3
4
로그인 성공
-> JWT 생성
-> Cookie 생성
-> Controller 응답
이 흐름이 각각의 테스트에서 나누어 검증되면, 문제가 생겼을 때 어느 계층에서 깨졌는지 찾기 쉬워진다.
JwtCookieProviderTest 추가
오늘 새로 추가한 테스트 중 하나는 JwtCookieProviderTest였다.
이 테스트는 Spring context 없이 순수 단위 테스트로 작성하는 것이 적절했다.
Cookie 생성 로직은 복잡한 의존성이 필요한 기능이 아니라, 입력값에 따라 ResponseCookie를 만드는 로직이기 때문이다.
검증한 내용은 다음과 같다.
- access token cookie 생성
- refresh token cookie 생성
- 삭제용 access token cookie 생성
- 삭제용 refresh token cookie 생성
- 삭제 쿠키의 value가 비어 있는지
- 삭제 쿠키의
maxAge = 0여부
삭제 쿠키는 로그아웃에서 중요하다.
클라이언트 쿠키를 삭제하려면 같은 이름과 path로 maxAge를 0으로 내려줘야 한다.
그래서 삭제 쿠키 테스트도 따로 두는 것이 좋다고 느꼈다.
이후에는 path, HttpOnly, SameSite까지 더 촘촘하게 검증하면 좋을 것 같다.
현재 전체 테스트 실패 원인
개별적으로는 JwtCookieProviderTest, UserControllerTest, UserServiceTest가 통과했다.
하지만 전체 ./gradlew test를 실행하니 JwtTokenProviderTest > createAccessToken_테스트()에서 실패했다.
실패 원인은 단순했다.
1
2
3
4
5
6
private JwtTokenProvider jwtTokenProvider;
@BeforeEach
void setUp() {
}
jwtTokenProvider가 초기화되지 않았기 때문에 테스트에서 createAccessToken()을 호출할 때 NullPointerException이 발생했다.
1
String accessToken = jwtTokenProvider.createAccessToken("test@example.com", UserRole.USER);
테스트 실패 메시지를 보면 복잡해 보일 수 있지만, 결국 테스트 대상 객체를 만들지 않은 문제였다.
JwtTokenProviderTest 작성 방향
다음 작업에서는 JwtTokenProviderTest를 정리해야 한다.
JwtTokenProvider는 Spring context 없이 직접 생성해서 테스트할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
private static final String SECRET =
"limitbuy-commerce-jwt-secret-key-for-test-development-only-please-change";
@BeforeEach
void setUp() {
jwtTokenProvider = new JwtTokenProvider(
SECRET,
1800000,
1209600000
);
}
JWT 테스트에서 중요한 것은 토큰 문자열 자체를 비교하는 것이 아니다.
JWT는 발급 시점, 만료 시간, 서명 등에 따라 문자열이 달라질 수 있다.
그래서 같은 secret으로 parsing한 뒤 claim을 검증하는 것이 더 적절하다.
확인할 내용은 다음과 같다.
- subject
- role claim
- type claim
- expiration
- access token과 refresh token의 차이
이렇게 검증하면 토큰이 단순히 생성되었는지가 아니라, 의도한 정보를 담고 있는지 확인할 수 있다.
오늘 배운 점
오늘 테스트를 정리하면서 가장 크게 느낀 것은 테스트도 책임을 나누어야 한다는 점이다.
컨트롤러 테스트에서 모든 비즈니스 조건을 검증하려고 하면 테스트가 무거워진다.
반대로 서비스 테스트에서 HTTP status나 cookie를 검증하려고 하면 계층이 어긋난다.
각 테스트는 자신이 맡은 책임을 검증해야 한다.
- Controller는 요청과 응답
- Service는 비즈니스 흐름
- Provider는 값 생성 규칙
이 기준을 지키면 테스트가 더 읽기 쉬워지고, 실패했을 때 원인을 찾기도 쉬워진다.
마무리
오늘 작업은 기능 추가보다는 테스트의 신뢰도를 높이는 작업이었다.
로그인은 서비스의 입구이기 때문에 응답 body뿐 아니라 JWT 생성, Cookie 생성, 실패 흐름까지 함께 검증해야 한다.
아직 JwtTokenProviderTest가 실패하고 있지만, 원인은 명확하다.
다음 작업에서는 테스트 대상 객체를 직접 초기화하고, 토큰을 parsing해서 claim을 검증하는 방식으로 정리하면 될 것 같다.
테스트를 작성할수록 내가 만든 인증 흐름을 더 정확히 이해하게 된다.
그래서 테스트는 단순히 버그를 잡는 도구가 아니라, 설계를 다시 확인하는 도구라고 느꼈다.
Leave a comment