fragile and resilient

Spring

[Spring, AOP] 테스트 격리를 위한 MockRestServiceServer 초기화

Green Lawn 2022. 10. 11. 19:39

모아모아 프로젝트에서 외부 API와 연동된 테스트를 진행하면서 겪었던 이슈에 대해 정리해 보고자 합니다.

문제 상황

이번 스프린트에서 슬랙 알림을 도입하기로 하여 알림 로직을 구현하였는데요.
기능을 구현하는 데에는 문제가 없었지만, 알림 기능 테스트를 진행하면서 해당 오류를 마주쳤습니다.

오류는 MockRestServiceServer에서 발생하고 있었는데요. 내용을 살펴보면 예상되지 않은 추가 요청이라는 문구를 확인할 수 있었습니다.

본격적으로 문제를 살펴보기 전에 간단하게 프로젝트 로직에 대해 설명해 드리겠습니다.

프로젝트에서는 외부 API와 통신하며 동작하는 기능(Github OAuth, Slack Alarm)이 있는데요. 해당 작업은 RestTemplate를 사용해서 통신하고 있습니다.

(RestTemplate은 Spring에서 제공하는 HTTP 통신에 유용하게 쓸 수 있는 템플릿입니다. )

MockRestServiceServer는 이러한 RestTemplate를 테스트하기 위해 Spring에서 제공하는 테스트 라이브러리입니다.

따라서 RestTemplate를 mocking 할 수 있는 기능을 제공하고 있기 때문에, 해당 라이브러리를 사용해서 외부 API와 결합된 기능을 테스트하고 있었습니다.

다시 위의 오류로 넘어가서 해당 예외가 발생하는 원인을 파악해 보겠습니다.

org.springframework.test.web.client.SimpleRequestExpectationManager

예외는 해당 로직에서 발생하고 있었는데요.
일치하는 요청 expectation이 없고 expectationIterator 값이 없다면 예외가 발생하고 있었습니다.

요청 expectation를 등록하는 테스트 코드를 살펴보겠습니다.

public MockRestServiceServer mockServer;

public void sendAlarm(SlackMessageRequest slackMessageRequest) {
        //..   
        
        try {
            mockServer.expect(requestTo("slack-uri"))
                    .andExpect(method(HttpMethod.GET))
                    .andExpect(header("Authorization", slackAuthorization))
                    .andRespond(withStatus(HttpStatus.OK)
                            .contentType(MediaType.APPLICATION_JSON)
                            .body(objectMapper.writeValueAsString(slackUsersResponse)));
        //..                  

        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
}

여기서 mockServer.expect(requestTo("slack-uri")) 로직처럼, expect 메서드를 통해 요청 expectation을 등록하고 있습니다.

여기서 expect 메서드를 들어가서 살펴보면,

요청 expectation 수가 1개보다 클 경우에는, 첫 번째 expectation이 선언 순서와 일치해야 한다는 설명이 있었습니다.
위의 글을 읽고 오류가 발생한 원인에 대한 이유를 파악할 수 있었는데요.

현재 테스트에서 MockRestServiceServer가 쓰이는 메서드들을 살펴보면, 굉장히 여러 곳에서 같은 MockRestServiceServer를 사용하고 있는 것을 알 수 있습니다.

Github OAuth, Slack Alarm 테스트에 쓰이는 mockRestServiceServer 세팅 로직

결론적으로 앞의 설명과 위에서 발생한 오류 문구를 종합하여 예외가 발생한 이유를 예상해 보면,
MockRestServiceServer는 expectation들이 등록된 순서의 영향을 받는 데, 여러 곳에서 사용되고 있기 때문에 순서에 대한 격리가 이루어지지 않아 예외가 발생하고 있다고 생각했습니다.

따라서 위의 문제를 안전하게 해결하기 위해서는, MockRestServiceServer를 사용하고 난 이후에는 초기화해주는 것이 적절하다고 생각했습니다.
여기서 문제는 각각의 테스트마다 초기화해야 하는 것이 아닌(@BeforEach로 해결 불가능), 테스트 메서드 내부에서 사용되는 특정 메서드마다 초기화를 해주어야 했습니다.

비즈니스 테스트 로직과 부가(reset) 로직이 섞여있다.
매우 여러 곳에서 쓰이는 reset()...

따라서 위 코드처럼 mockingSlackAlarm() 메서드를 호출하기 전에, MockRestServiceServer를 reset()하는 메서드를 호출했습니다.
기대했듯 더 이상 위의 오류는 발생하지 않았지만, 굉장히 문제가 많은 코드로 보였습니다.

위 로직의 문제점

1. 위 사진에서 보실 수 있듯이, 테스트하려는 비즈니스 로직과 부수적인 로직(MockRestServerService reset())이 섞여 있는 것을 볼 수 있습니다.
2. 스터디 참여 이전이나 이후에, 개발자가 실수로 reset하지 않으면 테스트가 깨지게 됩니다.
3. 지금은 슬랙 알림을 보내는 곳이 스터디 참여하기 기능에서 밖에 없습니다.
하지만 추후 추가될 공지사항과 커뮤니티 등 여러 곳에서 슬랙 알림이 추가되면 초기화해야 하는 곳이 매우 증가할 것입니다.
여러 곳에서 테스트하려는 비즈니스 로직과 MockRestServerService 초기화 로직이 섞여 있다면 실수할 가능성이 매우 높다고 생각합니다.

AOP를 활용한 MockRestServiceServer 초기화

여러 테스트에서 쓰이는 MockRestServerService mocking 메서드는 아래와 같은 특징이 있었습니다.

따라서 MockResrServiceServer 초기화()인 횡단 관심사를 해결하기 위해 AOP를 활용하고자 했습니다.

1. 우선 슬랙 알림을 mocking한 로직을 참여한다() 메서드 내부로 넣었고,

2. AOP class를 생성하여, sendAlarm() 메서드를 호출하기 전마다 MockResrServiceServer를 초기화해주도록 했습니다.

SlackAlarmMockServer.class

개선 결과

해당 개선을 통해 테스트 코드에서는

비즈니스 로직을 테스트하는 데에만 집중할 수 있게 되었습니다.