Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 단위 테스트 코드 추가 #133

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.solidconnection.unit.service;
package com.example.solidconnection.unit.application.service;

import com.example.solidconnection.application.domain.Application;
import com.example.solidconnection.application.domain.Gpa;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package com.example.solidconnection.unit.auth.service;

import com.example.solidconnection.auth.dto.ReissueResponse;
import com.example.solidconnection.auth.service.AuthService;
import com.example.solidconnection.config.token.TokenService;
import com.example.solidconnection.config.token.TokenType;
import com.example.solidconnection.custom.exception.CustomException;
import com.example.solidconnection.custom.exception.ErrorCode;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import com.example.solidconnection.type.Gender;
import com.example.solidconnection.type.PreparationStatus;
import com.example.solidconnection.type.Role;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.time.LocalDate;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;
Comment on lines +27 to +29
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와일드카드 *는 사용하지 않는게 좋을 것 같네요!
인텔리제이에서 설정하는 방법은 링크를 참고하시면 됩니다.

https://giantdwarf.tistory.com/58


@ExtendWith(MockitoExtension.class)
@DisplayName("인증 서비스 테스트")
class AuthServiceTest {

@InjectMocks
private AuthService authService;

@Mock
private TokenService tokenService;

@Mock
private SiteUserRepository siteUserRepository;

@Mock
private RedisTemplate<String, String> redisTemplate;

@Mock
private ValueOperations<String, String> valueOperations;

private static final String TEST_ACCESS_TOKEN = "testAccessToken";
private static final String TEST_REFRESH_TOKEN = "testRefreshToken";
private static final String SIGN_OUT_VALUE = "signOut";

private SiteUser testUser;

@BeforeEach
void setUp() {
testUser = createTestUser();
}

@Test
@DisplayName("로그아웃_요청시_리프레시_토큰을_무효화한다()")
Comment on lines +61 to +62
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존 코드 컨벤션을 지키지 않고 DisplayName 어노테이션을 사용한 이유가 있나요?

void shouldInvalidateRefreshTokenWhenSignOut() {
// given
String refreshTokenKey = TokenType.REFRESH.addTokenPrefixToSubject(testUser.getEmail());
when(redisTemplate.opsForValue()).thenReturn(valueOperations);

// when
authService.signOut(testUser.getEmail());

// then & verify
verify(valueOperations).set(
eq(refreshTokenKey),
eq(SIGN_OUT_VALUE),
eq(604800000L),
eq(TimeUnit.MILLISECONDS)
);
Comment on lines +71 to +77
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 테스트 코드는 결과가 아니라 행위를 검증하고 있는 것 같네요, 테스트 코드는 세부 구현을 담으면 위험합니다. 예를 들어 '값을 저장하는 시간'을 604800000L 에서 604800001L로 변경한다면 위 테스트 코드는 깨질 것입니다. 저라면 리프레시 토큰이 무효화되었다는 결과만 검증할 것 같습니다. 관련 글을 첨부합니다!

https://ojt90902.tistory.com/1364 [테스트로 유출된 도메인 지식] 부분
https://www.youtube.com/watch?v=R7spoJFfQ7U [세부 구현에 의존적인 테스트] 부분

}


@Test
@DisplayName("회원탈퇴_요청시_탈퇴일자를_설정한다()")
void shouldSetQuitedAtWhenUserQuits() {
// given
when(siteUserRepository.getByEmail(testUser.getEmail())).thenReturn(testUser);

// when
authService.quit(testUser.getEmail());

// then
assertThat(testUser.getQuitedAt()).isNotNull();
assertThat(testUser.getQuitedAt()).isEqualTo(LocalDate.now().plusDays(1));
Comment on lines +91 to +92
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

중복 검증을 하고 있는 것 같습니다😔
assertThat(testUser.getQuitedAt()).isEqualTo(LocalDate.now().plusDays(1)); 가 곧 not null 이라는 것을 포함하고 있지 않나요? 91번 라인은 지우는게 좋겠습니다.

테스트 코드에서 가독성은 굉장히 중요한 요소라고 생각합니다. 테스트하고자 하는 것이 무엇인지, 그것을 검증하기 위해서 반드시 필요한 것들만 있는지 다시 생각해보시면 더 좋을 것 같아요.


// verify
verify(siteUserRepository).getByEmail(testUser.getEmail());
Comment on lines +94 to +95
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

대부분의 테스트 코드에 verify 를 사용하신 이유가 무엇인지 듣고 싶네요!

}

@Test
@DisplayName("존재하지_않는_이메일로_회원탈퇴_요청시_예외를_반환한다()")
void shouldThrowExceptionWhenQuitWithNonExistentEmail() {
// given
when(siteUserRepository.getByEmail(testUser.getEmail()))
.thenThrow(new CustomException(ErrorCode.USER_NOT_FOUND));

// when & then
CustomException exception = assertThrows(CustomException.class,
() -> authService.quit(testUser.getEmail()));
assertThat(exception.getCode()).isEqualTo(ErrorCode.USER_NOT_FOUND.getCode());

// verify
verify(siteUserRepository).getByEmail(testUser.getEmail());
}

@Test
@DisplayName("유효한_리프레시_토큰으로_재발급_요청시_새로운_액세스_토큰을_반환한다()")
void shouldReturnNewAccessTokenWhenRefreshTokenValid() {
// given
String refreshTokenKey = TokenType.REFRESH.addTokenPrefixToSubject(testUser.getEmail());
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(valueOperations.get(refreshTokenKey)).thenReturn(TEST_REFRESH_TOKEN);
when(tokenService.generateToken(testUser.getEmail(), TokenType.ACCESS)).thenReturn(TEST_ACCESS_TOKEN);

// when
ReissueResponse response = authService.reissue(testUser.getEmail());

// then
assertThat(response.accessToken()).isEqualTo(TEST_ACCESS_TOKEN);

// verify
verify(valueOperations).get(refreshTokenKey);
verify(tokenService).generateToken(testUser.getEmail(), TokenType.ACCESS);
verify(tokenService).saveToken(TEST_ACCESS_TOKEN, TokenType.ACCESS);
}

@Test
@DisplayName("만료된_리프레시_토큰으로_재발급_요청시_예외를_반환한다()")
void shouldThrowExceptionWhenRefreshTokenExpired() {
// given
String refreshTokenKey = TokenType.REFRESH.addTokenPrefixToSubject(testUser.getEmail());
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(valueOperations.get(refreshTokenKey)).thenReturn(null);

// when & then
CustomException exception = assertThrows(CustomException.class,
() -> authService.reissue(testUser.getEmail()));
assertThat(exception.getCode()).isEqualTo(ErrorCode.REFRESH_TOKEN_EXPIRED.getCode());

// verify
verify(valueOperations).get(refreshTokenKey);
verify(tokenService, never()).generateToken(any(), any());
}

private SiteUser createTestUser() {
return new SiteUser(
"[email protected]",
"nickname",
"profileImageUrl",
"1999-10-21",
PreparationStatus.CONSIDERING,
Role.MENTEE,
Gender.MALE
);
}
}
Comment on lines +162 to +164
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

파일 끝 개행은 왜 해야하는걸까요?

https://hyeon9mak.github.io/github-no-newline-at-a-end-of-file/

Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package com.example.solidconnection.unit.auth.service;

import com.example.solidconnection.auth.client.KakaoOAuthClient;
import com.example.solidconnection.auth.dto.SignInResponse;
import com.example.solidconnection.auth.dto.kakao.FirstAccessResponse;
import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest;
import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse;
import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto;
import com.example.solidconnection.auth.service.SignInService;
import com.example.solidconnection.config.token.TokenService;
import com.example.solidconnection.config.token.TokenType;
import com.example.solidconnection.custom.exception.CustomException;
import com.example.solidconnection.custom.exception.ErrorCode;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import com.example.solidconnection.type.Gender;
import com.example.solidconnection.type.PreparationStatus;
import com.example.solidconnection.type.Role;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.LocalDate;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
@DisplayName("카카오 로그인 서비스 테스트")
class SignInServiceTest {

@InjectMocks
private SignInService signInService;

@Mock
private SiteUserRepository siteUserRepository;

@Mock
private TokenService tokenService;

@Mock
private KakaoOAuthClient kakaoOAuthClient;

private static final String TEST_ACCESS_TOKEN = "testAccessToken";
private static final String TEST_REFRESH_TOKEN = "testRefreshToken";
private static final String TEST_KAKAO_OAUTH_TOKEN = "testKakaoOauthToken";
private static final String VALID_CODE = "validCode";
private static final String INVALID_CODE = "invalidCode";

private SiteUser testUser;
private KakaoUserInfoDto testKakaoUserInfo;
private KakaoCodeRequest validKakaoCodeRequest;

@BeforeEach
void setUp() {
testUser = createTestUser();
testKakaoUserInfo = createTestKakaoUserInfoDto();
validKakaoCodeRequest = new KakaoCodeRequest(VALID_CODE);
}

@Test
@DisplayName("기존_회원이_로그인하면_로그인_정보를_반환한다()")
void shouldReturnSignInInfoWhenUserAlreadyRegistered() {
// given
when(kakaoOAuthClient.processOauth(VALID_CODE)).thenReturn(testKakaoUserInfo);
when(siteUserRepository.existsByEmail(testUser.getEmail())).thenReturn(true);
when(siteUserRepository.getByEmail(testUser.getEmail())).thenReturn(testUser);
when(tokenService.generateToken(testUser.getEmail(), TokenType.ACCESS)).thenReturn(TEST_ACCESS_TOKEN);
when(tokenService.generateToken(testUser.getEmail(), TokenType.REFRESH)).thenReturn(TEST_REFRESH_TOKEN);
Comment on lines +66 to +74
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BDD mockito 가 아니라 그냥 mockito 를 선호하시는 것 같은데, 특별한 이유가 있나요?


// when
KakaoOauthResponse response = signInService.signIn(validKakaoCodeRequest);

// then
assertThat(response).isInstanceOf(SignInResponse.class);
SignInResponse signInResponse = (SignInResponse) response;
assertThat(signInResponse.accessToken()).isEqualTo(TEST_ACCESS_TOKEN);
assertThat(signInResponse.refreshToken()).isEqualTo(TEST_REFRESH_TOKEN);
assertThat(signInResponse.isRegistered()).isTrue();

// verify
verify(kakaoOAuthClient).processOauth(VALID_CODE);
verify(siteUserRepository).existsByEmail(testUser.getEmail());
verify(siteUserRepository).getByEmail(testUser.getEmail());
verify(tokenService).generateToken(testUser.getEmail(), TokenType.ACCESS);
verify(tokenService).generateToken(testUser.getEmail(), TokenType.REFRESH);
verify(tokenService).saveToken(TEST_REFRESH_TOKEN, TokenType.REFRESH);
}

@Test
@DisplayName("탈퇴한_회원이_로그인하면_탈퇴일자를_초기화하고_로그인_정보를_반환한다()")
void shouldResetQuitedAtAndReturnSignInInfoWhenQuitedUserSignsIn() {
// given
testUser.setQuitedAt(LocalDate.now().minusDays(1));

when(kakaoOAuthClient.processOauth(VALID_CODE)).thenReturn(testKakaoUserInfo);
when(siteUserRepository.existsByEmail(testUser.getEmail())).thenReturn(true);
when(siteUserRepository.getByEmail(testUser.getEmail())).thenReturn(testUser);
when(tokenService.generateToken(testUser.getEmail(), TokenType.ACCESS)).thenReturn(TEST_ACCESS_TOKEN);
when(tokenService.generateToken(testUser.getEmail(), TokenType.REFRESH)).thenReturn(TEST_REFRESH_TOKEN);

// when
KakaoOauthResponse response = signInService.signIn(validKakaoCodeRequest);

// then
assertThat(response).isInstanceOf(SignInResponse.class);
assertThat(testUser.getQuitedAt()).isNull();

// verify
verify(siteUserRepository).getByEmail(testUser.getEmail());
}

@Test
@DisplayName("신규_회원이_로그인하면_회원가입_정보를_반환한다()")
void shouldReturnSignUpInfoWhenUserNotRegistered() {
// given
when(kakaoOAuthClient.processOauth(VALID_CODE)).thenReturn(testKakaoUserInfo);
when(siteUserRepository.existsByEmail(testUser.getEmail())).thenReturn(false);
when(tokenService.generateToken(testUser.getEmail(), TokenType.KAKAO_OAUTH)).thenReturn(TEST_KAKAO_OAUTH_TOKEN);

// when
KakaoOauthResponse response = signInService.signIn(validKakaoCodeRequest);

// then
assertThat(response).isInstanceOf(FirstAccessResponse.class);
FirstAccessResponse firstAccessResponse = (FirstAccessResponse) response;
assertThat(firstAccessResponse.kakaoOauthToken()).isEqualTo(TEST_KAKAO_OAUTH_TOKEN);
assertThat(firstAccessResponse.isRegistered()).isFalse();
assertThat(firstAccessResponse.email()).isEqualTo(testUser.getEmail());
assertThat(firstAccessResponse.nickname()).isEqualTo("testNickname");
assertThat(firstAccessResponse.profileImageUrl()).isEqualTo("testProfileImageUrl");

// verify
verify(siteUserRepository).existsByEmail(testUser.getEmail());
verify(tokenService).generateToken(testUser.getEmail(), TokenType.KAKAO_OAUTH);
verify(tokenService).saveToken(TEST_KAKAO_OAUTH_TOKEN, TokenType.KAKAO_OAUTH);
}

@Test
@DisplayName("유효하지_않은_인증_코드로_로그인_시도하면_예외를_반환한다()")
void shouldThrowExceptionWhenInvalidAuthCodeProvided() {
// given
KakaoCodeRequest invalidRequest = new KakaoCodeRequest(INVALID_CODE);
when(kakaoOAuthClient.processOauth(INVALID_CODE))
.thenThrow(new CustomException(ErrorCode.INVALID_OR_EXPIRED_KAKAO_AUTH_CODE));

// when & then
CustomException exception = assertThrows(CustomException.class,
() -> signInService.signIn(invalidRequest));
assertThat(exception.getCode()).isEqualTo(ErrorCode.INVALID_OR_EXPIRED_KAKAO_AUTH_CODE.getCode());

// verify
verify(kakaoOAuthClient).processOauth(INVALID_CODE);
}

@Test
@DisplayName("카카오_리다이렉트_URI가_일치하지_않으면_예외를_반환한다()")
void shouldThrowExceptionWhenKakaoRedirectUriMismatch() {
// given
when(kakaoOAuthClient.processOauth(VALID_CODE))
.thenThrow(new CustomException(ErrorCode.KAKAO_REDIRECT_URI_MISMATCH));

// when & then
CustomException exception = assertThrows(CustomException.class,
() -> signInService.signIn(validKakaoCodeRequest));
assertThat(exception.getCode()).isEqualTo(ErrorCode.KAKAO_REDIRECT_URI_MISMATCH.getCode());

// verify
verify(kakaoOAuthClient).processOauth(VALID_CODE);
}

@Test
@DisplayName("카카오_사용자_정보_조회에_실패하면_예외를_반환한다()")
void shouldThrowExceptionWhenKakaoUserInfoFetchFails() {
// given
when(kakaoOAuthClient.processOauth(VALID_CODE))
.thenThrow(new CustomException(ErrorCode.KAKAO_USER_INFO_FAIL));

// when & then
CustomException exception = assertThrows(CustomException.class,
() -> signInService.signIn(validKakaoCodeRequest));
assertThat(exception.getCode()).isEqualTo(ErrorCode.KAKAO_USER_INFO_FAIL.getCode());

// verify
verify(kakaoOAuthClient).processOauth(VALID_CODE);
}

private SiteUser createTestUser() {
return new SiteUser(
"[email protected]",
"nickname",
"profileImageUrl",
"1999-10-21",
PreparationStatus.CONSIDERING,
Role.MENTEE,
Gender.MALE
);
}

private KakaoUserInfoDto createTestKakaoUserInfoDto() {
KakaoUserInfoDto.KakaoAccountDto kakaoAccountDto = new KakaoUserInfoDto.KakaoAccountDto(
new KakaoUserInfoDto.KakaoAccountDto.KakaoProfileDto("testProfileImageUrl", "testNickname"),
testUser.getEmail());
return new KakaoUserInfoDto(kakaoAccountDto);
}
}
Loading