본문으로 바로가기
반응형

개요

스프링 환경에서 테스트 코드를 작성하다보면 Static 메소드를 모킹해야하는 상황이 발생하는데, 일반적인 케이스랑은 조금 다르다. 본 예제에서는 Slack SDK를 활용하여 슬랙에 메시지를 전송하는 상황을 예제로 들어 Static 메소드를 모킹하고 검증하는 방법에 대해 기술한다.

Slack SDK

dependencies {
    implementation("com.slack.api:slack-api-client:1.28.0")
}

먼저 build.gradle.kts에 위와 같이 슬랙 의존성을 추가한다.

How

public class SlackClient {

    @Value("${spring.slack.token}")
    private String token;

    public void send(SlackMessage message) {
        MethodsClient client = Slack.getInstance().methods();
        ChatPostMessageRequest request =
                ChatPostMessageRequest.builder()
                        .token(token)
                        .username(message.getUsername())
                        .channel(message.getChannel())
                        .text(message.getText())
                        .iconEmoji(message.getIconEmoji())
                        .build();

        ChatPostMessageResponse response = postMessage(client, request);
        ...
    }

    private ChatPostMessageResponse postMessage(
            MethodsClient client, ChatPostMessageRequest request) {
        try {
            return client.chatPostMessage(request);
        } catch (SlackApiException | IOException e) {
            log.warn("SlackClient | exc={}", e.getMessage());
            return null;
        }
    }
}

예를 들어 위와 같은 코드가 있다고 가정한다. send() 메소드는 외부에서 슬랙에 메시지를 전송하기 위해 호출하는 메소드이며 내부적으로 슬랙 클라이언트 관련 설정을 하고 postMessage() 메소드를 통해 실제 슬랙에 메시지를 전송한다.

먼저 Slack.getInstance()를 실제 코드로 들어가보면 

public class Slack implements AutoCloseable {
	private static final Slack SINGLETON = new Slack(SlackConfig.DEFAULT, new SlackHttpClient());
	...
    
	public static Slack getInstance() {
        return SINGLETON;
    }
    ...
 }

위와 같은 모습을 확인할 수 있다. 내부적으로 static 변수에 Slack 인스턴스를 할당하고 getInstance()에서는 해당 인스턴스를 리턴하는 작업만 진행한다. 위와 같은 Static 메소드의 경우 일반적인 방식으로 모킹할 수 없다.

mockStatic

구글에 검색해보면 Static 메소드를 모킹하는 방법으로 PowerMock을 사용하는 방법이 많이 나오는데, 이것 하나때문에 추가적인 의존성을 설치하고 싶진 않았다. 그래서 좀 더 찾아보니 mockito 특정 버전 이후부터 mockStatic을 통해 모킹을 할 수 있다고 한다.

class SlackClientTest {
    @Mock MethodsClient methodsClient;

    @Mock Slack slack;

    @InjectMocks SlackClient slackClient;

    MockedStatic<Slack> slackMockedStatic;
    ...
}

먼저 위처럼 MockedStatic으로 감싼 Slack 객체를 하나 정의한다. 위에서 설명했지만 Slack.getInstance()가 Static 메소드이고 해당 메소드를 모킹해줘야 하기 때문이다.

@BeforeEach
void setUp() {
    slackMockedStatic = mockStatic(Slack.class);
    when(Slack.getInstance()).thenReturn(slack);
    when(slack.methods()).thenReturn(methodsClient);
    ReflectionTestUtils.setField(slackClient, "token", "token");
}

그리고 BeforeEach를 통해 MockedStatic으로 감싼 변수에 mockStatic을 통해 클래스를 모킹하여 대입해준다. 아래 when라인은 기존 리턴값을 모킹해줄 때와 동일하게 작업해주면 된다. 가장 아래 라인에 있는 ReflectionTestUtils.setField()의 경우 SlackClient에서 @Value() 어노테이션을 통해 환경변수값을 주입시켜주고 있기 때문에 추가해줬다.

@AfterEach
void tearDown() {
    slackMockedStatic.close();
}

그리고 위처럼 각 테스트가 종료될 때 모킹한 객체를 close해줘야한다. 한번 모킹한 클래스를 다시 모킹하려고 하기 때문인데, 위 라인이 없으면 For com.slack.api.Slack, static mocking is already registered in the current thread 에러가 발생한다.

나머지 테스트 코드는 기존과 동일하게 작성하면 정상적으로 동작하게 된다.

반응형