Junit5 로그 테스트하는 방법

소개

  • 테스트 코드를 작성할 때 로그가 정상적으로 출력 됐는지 검증하고자 할 때가 있다.
  • 테스트 코드에서 로그를 검증하고자 하는 2가지 방법에 대해서 알아보자.

첫번재 방법

Junit5 환경에서 OutputCaptureExtension.class 사용해서 콘솔에 출력되는 로그를 캡쳐 후 원하는 로그가 출력되었는지 확인하는 방법이다. 샘플 코드는 @Slf4j를 사용해서 로그를 출력한다.

Sample Service Code
아래 코드는 userId를 통해 UserInfo를 조회할 때 User가 없다면 예외 처리를 하는 로직이다. throw를 이용해 예외 처리를 넘기지 않고 단순히 log로만 예외 처리를 하는 방식이다.

@Slf4j
@Service
@RequiredArgsConstructor
public class userService {

                private final UserRepository userRepository;

		public UserInfo getUserInfo(Long userId) {
				try {
						UserInfo userInfo = userRepository.findUserInfo(userId);
						log.debug("success get userInfo")
						return userInfo;
				} catch(EntityNotFoundException e) {
						log.debug("failed get userInfo")
						log.warn("no entity By {}", userId);
						코드가 실패했을 때 로그가 정상적으로 찍혔는지 확인하는 방법이다.
			  }
		}
}

Sample Test Code

package com.test.junit;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;

@ExtendWith(OutputCaptureExtension.class)
public class userServiceTest {

    @Autowired
    UserService userService;

    @Test
    void given_userId_when_getUserInfo_then_generate_error_log   (CapturedOutput capturedOutput) {
	       
	        Long userId = 1L;
                userService.getUserInfo(userId);
                assertThat(capturedOutput.toString(), containsString("failed get userInfo"));
                assertThat(capturedOutput.toString(), containsString("no entity By 1"));
        
    }
}

결론

간단하게 로그에 대한 검증 테스트를 할 수 있다는 장점이 있고 application.yml 파일에서 로그 Level 설정을 통해서 원하는 Level의 로그만 따로 검증 테스트를 할 수 있다.


두번째 방법

LoggerFactory는 로그 메시지를 기록하는 Logger를 생성할 때 사용된다. Logger에 기록된 로그들을 Slf4j의 구현체인 ch.qos.logback를 통해서
실제로 출력된 로그들을 저장하고 검증할 수 있다.

Gradle Dependencies

dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    implementation 'ch.qos.logback:logback-classic:1.2.11' // Slf4j의 구현체
}

Sample Service Code

아래는 각각의 로그 level 별로 동일한 메시지를 출력하는 메소드이다.

package com.test.junit.log;


import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CustomLog {

    public void generateLogs(String msg) {
        log.trace(msg);
        log.debug(msg);
        log.info(msg);
        log.warn(msg);
        log.error(msg);
    }



}

Slf4j의 구현체인 logback를 통해서 실제로 출력된 로그들을 관리할 수 있는 클래스이다.
countEventsForLogger 메소드는 몇 개의 로그 출력 이벤트가 발생되었는지, search 메소드는 출력된 로그 msg와 level을 찾아서 List 형태로 반환해주며, contains는 단순히 msg와 level에 맞는 로그가 있는지 검증만 해준다.

package com.test.junit.log;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

public class MemoryAppender extends ListAppender<ILoggingEvent> {

    public void reset() {
        this.list.clear();
    }

    public boolean contains(String string, Level level) {
        return this.list.stream()
                .anyMatch(event -> event.toString().contains(string)
                        && event.getLevel().equals(level));
    }

    public int countEventsForLogger(String loggerName) {
        return (int) this.list.stream()
                .filter(event -> event.getLoggerName().contains(loggerName))
                .count();
    }

    public List<ILoggingEvent> search(String string) {
        return this.list.stream()
                .filter(event -> event.toString().contains(string))
                .collect(Collectors.toList());
    }

    public List<ILoggingEvent> search(String string, Level level) {
        return this.list.stream()
                .filter(event -> event.toString().contains(string)
                        && event.getLevel().equals(level))
                .collect(Collectors.toList());
    }

    public int getSize() {
        return this.list.size();
    }

    public List<ILoggingEvent> getLoggedEvents() {
        return Collections.unmodifiableList(this.list);
    }
}

Sample Test Code
LoggerFactory를 통해 원하는 클래스의 LogWatch를 시작하고 로그의 level 까지도 커스텀 할 수 있다는 장점이 있다.
테스트를 시작하기 전에 start() 메소드를 통해 LogWatch를 시작하고, 테스트가 끝나면 stop()으로 LogWatch를 종료한다.

package com.test.junit;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import com.test.junit.log.CustomLog;
import com.test.junit.log.MemoryAppender;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;

import static org.assertj.core.api.Assertions.assertThat;

public class CustomLogTest {

    MemoryAppender memoryAppender;

    @BeforeEach
    public void setup() {

        Logger logger = (Logger) LoggerFactory.getLogger(CustomLog.class);
        memoryAppender = new MemoryAppender();
        memoryAppender.setContext((LoggerContext) LoggerFactory.getILoggerFactory());
        logger.setLevel(Level.DEBUG);
        logger.addAppender(memoryAppender);
        memoryAppender.start();

    }

    @AfterEach
    public void teardown() {
        memoryAppender.stop();
    }

    @Test
    public void test() {

        LogTest001 logTest001 = new LogTest001();
        logTest001.generateLogs("MSG");
        assertThat(memoryAppender.countEventsForLogger(CustomLog.class.getName())).isEqualTo(4);
        assertThat(memoryAppender.search("MSG", Level.INFO).size()).isEqualTo(1);
        assertThat(memoryAppender.contains("MSG", Level.TRACE)).isFalse();
    }

}

결론

Application 코드에서 테스트할 클래스의 로그 출력이 정상적으로 되었는지 검증할 수 있고 application.yml 같은 설정 파일이 아닌
코드에서 watch할 Log의 Level을 설정할 수 있다.

느낀점

간편하게 로그 검증을 하기 위해서는 OutputCaptureExtension.class를 사용해서 검증하는 것이 좋고, 세세히 검증을 해야 할 때는 직접 @Slf4j의 구현체를 사용해 Custom한 LogWatch를 구현해서 사용하는게 좋아 보인다.

Leave a Comment