@Component
@RequiredArgsConstructor
public class GoogleOAuth2 implements OAuth2 {
@Value("${spring.security.oauth2.client.registration.google.url}")
private String GOOGLE_SNS_LOGIN_URL;
@Value("${spring.security.oauth2.client.registration.google.client-id}")
private String GOOGLE_SNS_CLIENT_ID;
@Value("${spring.security.oauth2.client.registration.google.redirect-uri}")
private String GOOGLE_SNS_CALLBACK_URL;
@Value("${spring.security.oauth2.client.registration.google.client-secret}")
private String GOOGLE_SNS_CLIENT_SECRET;
@Value("${spring.security.oauth2.client.registration.google.scope}")
private String GOOGLE_DATA_ACCESS_SCOPE;
@Value("${spring.security.oauth2.client.registration.google.token-url}")
private String GOOGLE_TOKEN_REQUEST_URL;
@Value("${spring.security.oauth2.client.registration.google.user-info-url}")
private String GOOGLE_USER_INFO_REQUEST_URL;
private final ObjectMapper objectMapper;
private final RestTemplate restTemplate;
우리의 소셜로그인 관련 통신을 담당하는 GoogleOAuth2 클래스이다.
해당 클래스를 단위테스트로 테스트하려 했으나, 다음과 같은 문제가 있다.
또한, 전제조건은 다음과 같다.
따라서 위의 문제점을 해결하고자, 전제조건까지 고려하여 다음과 같은 해결방안들을 생각해 보았다.
나는 테스트 코드 작성을 위하여 프로덕션 코드를 변경하는 것은 좋지 않다고 생각한다.
또한 동시에 들었던 생각이 테스트하기 어려운 코드는 곧 잘못 작성된 코드이다. 라는 생각이 떠올랐지만, 당장 어떤 식으로 코드를 수정할지 감이 오질 않았다.
따라서 프로덕션 코드를 변경하지 않으면서 테스트 성능을 최대한 좋게 가져가기 위해 2,4번을 고민하였고
모든 Bean을 가져오는 것 보단 Reflection을 이용하는 것이 더 낫다고 판단하여 최종적으로 2번을 선택하였다.
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith({MockitoExtension.class})
class GoogleOAuth2ServiceTest {
@InjectMocks
GoogleOAuth2 googleOAuth2;
@Mock
ObjectMapper objectMapper;
@Mock
RestTemplate restTemplate;
String GOOGLE_SNS_LOGIN_URL = "GOOGLE_SNS_LOGIN_URL";
String GOOGLE_SNS_CLIENT_ID = "GOOGLE_SNS_CLIENT_ID";
String GOOGLE_SNS_CALLBACK_URL = "GOOGLE_SNS_CALLBACK_URL";
String GOOGLE_SNS_CLIENT_SECRET = "GOOGLE_SNS_CLIENT_SECRET";
String GOOGLE_DATA_ACCESS_SCOPE = "GOOGLE_DATA_ACCESS_SCOPE";
String GOOGLE_TOKEN_REQUEST_URL = "GOOGLE_TOKEN_REQUEST_URL";
String GOOGLE_USER_INFO_REQUEST_URL = "GOOGLE_USER_INFO_REQUEST_URL";
@BeforeAll
void setUp() {
ReflectionTestUtils.setField(googleOAuth2, "GOOGLE_SNS_LOGIN_URL", GOOGLE_SNS_LOGIN_URL);
ReflectionTestUtils.setField(googleOAuth2, "GOOGLE_SNS_CLIENT_ID", GOOGLE_SNS_CLIENT_ID);
ReflectionTestUtils.setField(googleOAuth2, "GOOGLE_SNS_CALLBACK_URL", GOOGLE_SNS_CALLBACK_URL);
ReflectionTestUtils.setField(googleOAuth2, "GOOGLE_SNS_CLIENT_SECRET", GOOGLE_SNS_CLIENT_SECRET);
ReflectionTestUtils.setField(googleOAuth2, "GOOGLE_DATA_ACCESS_SCOPE", GOOGLE_DATA_ACCESS_SCOPE);
ReflectionTestUtils.setField(googleOAuth2, "GOOGLE_TOKEN_REQUEST_URL", GOOGLE_TOKEN_REQUEST_URL);
ReflectionTestUtils.setField(googleOAuth2, "GOOGLE_USER_INFO_REQUEST_URL", GOOGLE_USER_INFO_REQUEST_URL);
}
@DisplayName("구글OAuth2로 리다이렉트 할 수 있는 url을 반환한다.")
@Test
void getOAuth2RedirectUrl_test() {
// given
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(GOOGLE_SNS_LOGIN_URL + "?")
.append("scope=" + GOOGLE_DATA_ACCESS_SCOPE + "&")
.append("response_type=" + "code" + "&")
.append("redirect_uri=" + GOOGLE_SNS_CALLBACK_URL + "&")
.append("client_id=" + GOOGLE_SNS_CLIENT_ID);
// when
String redirectUrl = stringBuilder.toString();
// then
assertThat(googleOAuth2.getOAuth2RedirectUrl()).isEqualTo(redirectUrl);
}
}
위와 같이 테스트를 작성하여 해결했다.
Either targetObject or targetClass for the field must be specified
java.lang.IllegalArgumentException: Either targetObject or targetClass for the field must be specified
ReflectionTestUtils.setField
메소드를 타고 들어가다 보면, 위와 같이 targetObject 혹은 targetClass가 null이 아니라는 assert문이 존재한다. @BeforeAll 어노테이션이 GoogleOAuth2가 인스턴스화 되기 이전에 실행되어서 그렇다고 생각했다.
이에 대해 https://github.com/mockito/mockito/issues/2563에서 mockito 개발자는 다음과 같이 언급한다.
This is intentional, as we don't want mocks to be shared across individual tests. Therefore, we want mocks to be fresh for each specific test.
If you require a mock to exist in the
@BeforeAll
, you would manually have to create one. However, we advise against doing so to avoid accidentally sharing stub state across test runs.
개별 테스트에서 mock객체가 공유되는것을 원하지 않고, 일부러 테스트마다 mock객체가 갱신되길 원하여 일부러 @BeforeAll이 작동되지 않도록 개발했다고 한다.
따라서, 테스트의 성능 감소를 무릅쓰고 @BeforeAll → @BeforeEach로 바꾸어 주었다.