소개
- 테스트 코드를 작성할 때
로그
가 정상적으로 출력 됐는지 검증하고자 할 때가 있다. - 테스트 코드에서 로그를 검증하고자 하는 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를 구현해서 사용하는게 좋아 보인다.