외부 API Mocking하여 테스트하기

주리링·2022년 8월 28일
1

우테코 생존기

목록 보기
16/17
post-thumbnail

github login을 구현하며 외부 api가 포함된 비지니스 로직을 테스트하기 위한 시도와 실패에 대한 포스팅입니다!

자세한 코드는 공식 팀의 깃허브에서 확인 하실 수 있습니다.

상황

현재 Github OAuth를 이용하여 위와 같은 순서의 로직으로 로그인 API를 구현하였습니다.
6, 8번과 같은 상황에서 아래 코드 처럼 외부 API를 사용하게 됩니다.

code를 이용하여 token을 발급받는 코드

BASE_URL+GITHUB_ACCESS_URL_SUFFIX = "https://github.com" + "/login/oauth/access_token" 으로 요청

private GithubAccessTokenResponse getGithubAccessToken(String code) {
    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
    headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
    GithubAccessTokenRequest accessTokenRequest =new GithubAccessTokenRequest(clientId, clientSecret, code);

    HttpEntity<GithubAccessTokenRequest> entity = new HttpEntity<>(accessTokenRequest, headers);
    GithubAccessTokenResponse accessTokenResponse = restTemplate.exchange(
BASE_URL+GITHUB_ACCESS_URL_SUFFIX,
            HttpMethod.POST,
            entity,
            GithubAccessTokenResponse.class
).getBody();

    validateToken(accessTokenResponse);
		return accessTokenResponse;
}

token을 이용하여 유저 프로필을 요청하는 코드

PROFILE_URL = "https://api.github.com/user" 으로 요청

private GithubProfileResponse getGithubProfile(GithubAccessTokenResponse accessTokenResponse) {
    String accessToken = accessTokenResponse.getAccessToken();
    String token = TOKEN + accessToken;
    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.add(HttpHeaders.AUTHORIZATION, token);
    httpHeaders.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);

    HttpEntity<Void> httpEntity =newHttpEntity<>(httpHeaders);
    GithubProfileResponse profileResponse = restTemplate.exchange(
PROFILE_URL,
            HttpMethod.GET,
            httpEntity,
            GithubProfileResponse.class,
            objectMapper
    ).getBody();

    validateProfile(profileResponse);
		return profileResponse;
}

이런 상황에서 외부 API를 Mocking하여 테스트를 시도했던 과정을 공유합니다.

시도 1 - 전략 패턴

github 서버와 요청을 주고 받는 인터페이스 OAuthClient를 만들어서 메인 코드에서 사용할 구현체와 테스트코드에서 사용할 구현체를 분리하여 테스트해야겠다고 생각했습니다.

외부 API를 테스트하기 위해 적용한 것

  1. OAuthClient 를 상속받는 FakeAuthClient 구현
public class FakeAuthClient implements OAuthClient{

		private static final String BASE_URL= "https://github.com";
		private static final String LOGIN_URL_SUFFIX= "/login/oauth/authorize?client_id=%s&redirect_uri=%s";
		private static final String CLIENT_ID= "clientId";
		private static final String REDIRECT_URL= "http://localhost:8080/callback";
		
		    @Override
		public String getRedirectUrl() {
				return String.format(BASE_URL+LOGIN_URL_SUFFIX,CLIENT_ID,REDIRECT_URL);
		    }
		
		    @Override
		public GithubProfileResponse getMemberProfile(String code) {
				return GithubClientFixtures.getGithubProfile(code);
		    }
}
  1. 인수테스트를 작성하며 서비스에 의존성을 주입하는 OAuthClient를 바꿔낄 방법이 없어서 fake api 생성
@RequestMapping("/api/auth")
@TestConstructor(autowireMode = AutowireMode.ALL)
@RestController
public classFakeAuthController {

		private final MemberRepository memberRepository;
		private final JwtTokenProvider jwtTokenProvider;
		private final AuthService authService;
		
		public FakeAuthController(MemberRepository memberRepository, JwtTokenProvider jwtTokenProvider) {
				this.memberRepository = memberRepository;
				this.jwtTokenProvider = jwtTokenProvider;
				this.authService = new AuthService(newFakeAuthClient(), memberRepository, jwtTokenProvider);
		    }
		
		    @PostMapping("/fake/token")
		public ResponseEntity<TokenResponse> login(@RequestBody OAuthCodeRequest OAuthCodeRequest) {
		        TokenResponse token = authService.generateAccessToken(OAuthCodeRequest);
				return ResponseEntity.ok(token);
		    }
}

현재 방법의 문제점

  1. Mocking 해야하는 객체에 전략패턴을 사용하기 위해 api를 만들고, 해당 api로 요청을 보내서 테스트를 하고 있다는 것이 어색하다.
  2. 가장 최소한의 mocking을 하고 싶은데 지금 방식은 그건 아니라고 판단된다.

해결 방안 1

그냥 OAuthClient 를 MockBean을 이용하여 Mocking

해결 방안 2

githubOAuthClient 가 요청을 보내는 것을 Mocking

방법

  1. 요청 보내는 url을 환경 변수로 암호화하여 테스트용 github url을 만들어서 처리
  2. restTemplate mocking
    MockRestServiceServer를 이용해 RestTemplate Test 하기

해결 방안 1보단 해결 방안 2가 더 최소한의 Mocking을 한다고 판단하였습니다.
해결 방안 2의 방법 모두 결과 및 효과는 똑같다고 판단되어 구현이 간편한 후자로 다시 외부 API 테스트를 진행하게 되었습니다.

시도 2 - RestTemplate Mocking

필요한 설정들

@Autowired
private RestTemplate restTemplate;

private MockRestServiceServer mockServer;

private ObjectMapper objectMapper = new ObjectMapper();

테스트 구현 부

private void mockGithubServer(String githubId, String name, String avatarUrl)
throws JsonProcessingException, URISyntaxException {
    mockServer = MockRestServiceServer.createServer(restTemplate);//1
    mockServer.expect(requestTo("https://github.com/login/oauth/access_token"))//2
            .andExpect(method(HttpMethod.POST))//3
            .andRespond(withStatus(HttpStatus.OK)//4
                    .contentType(MediaType.APPLICATION_JSON)//5
                    .body(objectMapper.writeValueAsString(newTokenResponse("token1"))));//6

    mockServer.expect(requestTo(newURI("https://api.github.com/user")))
            .andExpect(method(HttpMethod.GET))
            .andRespond(withStatus(HttpStatus.OK)
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(objectMapper.writeValueAsString(
		newGithubProfileResponse(githubId, name, avatarUrl))));
}
  1. mockServer 생성
    매번 생성 해야하는 이유 → 하나의 URL에 대한 response값을 교체해야할 경우 매번 새로해야하기 때문입니다.

  2. 요청이 오길 기대하는 URL

  3. http method

  4. response status code

  5. response contenType

  6. response body

구현 후 문제

인수테스트에서 사용되는 API를 모두 Fixture 클래스에 static 메서드로 모아놓았습니다.
하지만 여기에 mockingGithubServer 메서드를 추가하게 된다면, mockingGithubServer에서 사용하는 RestTemplate, ObjectMapper도 static으로 해야합니다.

public static TokenResponse 로그인을_한다(RestTemplate restTemplate, GithubClientFixtures client) {
  try{
  	mockGithubServer(restTemplate, client.getGithubId(), client.getName(), client.getAvatarUrl());
      }catch(JsonProcessingException | URISyntaxException e) {
          e.printStackTrace();
      }
  returnRestAssured
              .given().log().all()
              .contentType(MediaType.APPLICATION_JSON_VALUE)
              .body(newOAuthCodeRequest(client.getCode()))
              .when()
              .post("/api/auth/fake/token")
              .then().log().all()
              .statusCode(HttpStatus.OK.value())
              .extract()
              .as(TokenResponse.class);
}

하지만 RestTemplate가 Bean이므로 static으로 설정할 수 없는 문제가 생겼습니다.

해결 방안

  1. TokenResponse tokenResponse = 로그인을_한다(restTemplate, 주디);
    이렇게 restTemplate만 각 인수테스트에서 생성 한 후 넘긴다.
    ⇒ 너무 가독성이 떨어지는 것 같기도 그래도 코드 중복을 줄이려면 이게 제일 낫다고 판단된다.
  2. 각 인수 테스트에서 mockgithubserver 메서드 넣기
    ⇒ 이도저도 아닌 느낌..중복 코드는 그대로 발생하고 가독성도 떨어지고..
  3. 각 인수 테스트에 로그인 API 넣기
    ⇒ 로그인 API가 각 각 인수테스크 클래스마다 중복된다.

해결 방안에 대한 문제

  1. 중복 코드
  2. 매번 restTemplate를 주입해야한다는 것 자체가 부자연스럽다.

생각한 해결 방안도 팀원들과 의견을 나눠봤을 때 가독성이 떨어진다는 단점과 중복 코드가 발생한다는 단점이 있어 다른 방법을 통해 테스트를 진행했습니다.

시도 3 - github server mocking

GithubOAuthClient가 요청을 보내는 것을 Mocking하는 방안 중 1안인 요청 보내는 url을 환경 변수로 암호화하여 테스트용 github url을 만들어서 처리하도록 다시 외부 API 테스트를 진행하기로 하였습니다.

외부 API를 테스트하기 위해 적용한 것

  1. local과 test환경에서 외부로 요청할 api 도메인 환경 변수로 바꾸기
  • local : 실제 github 도메인
  • test : 테스트에서 사용하는 port번호를 포함하는 로컬 주소 + 관련 api
    업로드중..
  1. GithubOAuthClient에서 필드로 환경 변수로 설정한 api 도메인 주입
public GithubOAuthClient(
            @Value("${security.oauth2.client-id}") String clientId,
            @Value("${security.oauth2.client-secret}") String clientSecret,
            @Value("${github.url.base}") String baseUrl,
            @Value("${github.url.profile}") String profileUrl,
            @Value("${github.url.redirect}") String redirectUrl,
            ObjectMapper objectMapper,
            RestTemplate restTemplate
    ) {
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.baseUrl = baseUrl;
        this.profileUrl = profileUrl;
        this.redirectUrl = redirectUrl;
        this.objectMapper = objectMapper;
        this.restTemplate = restTemplate;
    }
  1. 테스트 환경에서 요청 보낼 외부 api 구현
@RequestMapping("/api/auth")
@RestController
public class FakeGithubController {

    @PostMapping("/login/oauth/access_token")
    public ResponseEntity<GithubAccessTokenResponse> getAccessToken(
            @RequestBody GithubAccessTokenRequest tokenRequest) {
        try {
            return ResponseEntity.ok(GithubClientFixtures.getAccessToken(tokenRequest.getCode()));
        } catch (IllegalStateException e) {
            return ResponseEntity.ok(null);
        }
    }

    @GetMapping("/user")
    public ResponseEntity<GithubProfileResponse> getProfile(
            @RequestHeader(value = HttpHeaders.AUTHORIZATION) String accessToken) {
        String token = accessToken.replaceAll("token ", "");
        try {
            return ResponseEntity.ok(GithubClientFixtures.getGithubProfileByToken(token));
        } catch (IllegalStateException e) {
            return ResponseEntity.ok(null);
        }
    }
}

현재는 github server mocking하는 방법으로 테스트를 진행하고 있습니다.
이유는 가장 최소한의 mocking으로 테스트 커버리지를 높일 수 있었고, 팀원들의 테스트 가독성과 저희팀에서는 아직까지 최소한의 유지보수 비용이 드는 테스트 방법이라고 생각했기 때문입니다.

테스트 커버리지 지표가 높다고 꼭 좋은 테스트는 아니지만, 외부 API를 테스트하는 다양한 방법을 생각해보는 기회가 있어서 재밌었습니다~~!

profile
코딩하는 감자

0개의 댓글