一、背景
我们写单元测试的时候,偶尔需要获取被测试对象的 logger 输出的内容,用于断言或者通过单元测试辅助自己排查问题。
比如:
(1)需要断言某个日志被输出过(不能仅仅将输出对象改为 Console 的 Appender 输出到控制台查看内容,无法通过 Assert 进行断言)
(2)某个方法比较复杂,中间多处打印日志,单测中 mock 依赖的对象之后,需要看到哪些日志被输出了。(运行单元测试时,通常不会输出到控制台,通常很多同学会临时在目标对象里添加打印语句,测试通过后删除,非常麻烦)
自己瞎想下:
(1)监听日志事件,获取事件内容进行打印或者断言(通常会和日志框架强相关)
(2)使用 Mockito 的 ArgumentCaptor 功能
(3)可以自己实现Logger
接口或者封装一个LoggerWrapper
作为外壳 ,测试时将Logger
mock 为我们自定义的Logger
类
在调用日志的方法时,将对应的内容存储到成员变量容器中
后面断言或者打印时,取出来即可。
今天介绍一个比较成熟的解决方案: log-captor
二、 介绍
GITHUB 地址:https://github.com/Hakky54/log-captor
最新版本:https://mvnrepository.com/artifact/io.github.hakky54/logcaptor
2021年11月22日 最新版本
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>logcaptor</artifactId>
<version>2.7.2</version>
<scope>test</scope>
</dependency>
2.1 常规测试
被测试类:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class FooService {
private static final Logger LOGGER = LogManager.getLogger(FooService.class);
public void sayHello() {
LOGGER.info("Keyboard not responding. Press any key to continue...");
LOGGER.warn("Congratulations, you are pregnant!");
}
}
单元测试:
import static org.assertj.core.api.Assertions.assertThat;
import nl.altindag.log.LogCaptor;
import org.junit.jupiter.api.Test;
public class FooServiceShould {
@Test
public void logInfoAndWarnMessages() {
LogCaptor logCaptor = LogCaptor.forClass(FooService.class);
FooService fooService = new FooService();
fooService.sayHello();
// Get logs based on level
assertThat(logCaptor.getInfoLogs()).containsExactly("Keyboard not responding. Press any key to continue...");
assertThat(logCaptor.getWarnLogs()).containsExactly("Congratulations, you are pregnant!");
// Get all logs
assertThat(logCaptor.getLogs())
.hasSize(2)
.contains(
"Keyboard not responding. Press any key to continue...",
"Congratulations, you are pregnant!"
);
}
}
2.2 复用 logCaptor
import nl.altindag.log.LogCaptor;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
public class FooServiceShould {
private static LogCaptor logCaptor;
private static final String EXPECTED_INFO_MESSAGE = "Keyboard not responding. Press any key to continue...";
private static final String EXPECTED_WARN_MESSAGE = "Congratulations, you are pregnant!";
@BeforeAll
public static setupLogCaptor() {
logCaptor = LogCaptor.forClass(FooService.class);
}
@AfterEach
public void clearLogs() {
logCaptor.clearLogs();
}
@AfterAll
public static void tearDown() {
logCaptor.close();
}
@Test
public void logInfoAndWarnMessagesAndGetWithEnum() {
FooService service = new FooService();
service.sayHello();
assertThat(logCaptor.getInfoLogs()).containsExactly(EXPECTED_INFO_MESSAGE);
assertThat(logCaptor.getWarnLogs()).containsExactly(EXPECTED_WARN_MESSAGE);
assertThat(logCaptor.getLogs()).hasSize(2);
}
@Test
public void logInfoAndWarnMessagesAndGetWithString() {
FooService service = new FooService();
service.sayHello();
assertThat(logCaptor.getInfoLogs()).containsExactly(EXPECTED_INFO_MESSAGE);
assertThat(logCaptor.getWarnLogs()).containsExactly(EXPECTED_WARN_MESSAGE);
assertThat(logCaptor.getLogs()).hasSize(2);
}
}
2.3 设置日志级别
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class FooService {
private static final Logger LOGGER = LogManager.getLogger(FooService.class);
public void sayHello() {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Keyboard not responding. Press any key to continue...");
}
LOGGER.info("Congratulations, you are pregnant!");
}
}
测试日志级别
import static org.assertj.core.api.Assertions.assertThat;
import nl.altindag.log.LogCaptor;
import org.junit.jupiter.api.Test;
public class FooServiceShould {
@Test
public void logInfoAndWarnMessages() {
LogCaptor logCaptor = LogCaptor.forClass(FooService.class);
logCaptor.setLogLevelToInfo();
FooService fooService = new FooService();
fooService.sayHello();
assertThat(logCaptor.getInfoLogs()).contains("Congratulations, you are pregnant!");
assertThat(logCaptor.getDebugLogs())
.doesNotContain("Keyboard not responding. Press any key to continue...")
.isEmpty();
}
}
2.4 异常日志
import nl.altindag.log.service.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class FooService {
private static final Logger LOGGER = LoggerFactory.getLogger(ZooService.class);
@Override
public void sayHello() {
try {
tryToSpeak();
} catch (IOException e) {
LOGGER.error("Caught unexpected exception", e);
}
}
private void tryToSpeak() throws IOException {
throw new IOException("KABOOM!");
}
}
异常日志断言
import static org.assertj.core.api.Assertions.assertThat;
import nl.altindag.log.LogCaptor;
import nl.altindag.log.model.LogEvent;
import org.junit.jupiter.api.Test;
public class FooServiceShould {
@Test
void captureLoggingEventsContainingException() {
LogCaptor logCaptor = LogCaptor.forClass(ZooService.class);
FooService service = new FooService();
service.sayHello();
List<LogEvent> logEvents = logCaptor.getLogEvents();
assertThat(logEvents).hasSize(1);
LogEvent logEvent = logEvents.get(0);
assertThat(logEvent.getMessage()).isEqualTo("Caught unexpected exception");
assertThat(logEvent.getLevel()).isEqualTo("ERROR");
assertThat(logEvent.getThrowable()).isPresent();
assertThat(logEvent.getThrowable().get())
.hasMessage("KABOOM!")
.isInstanceOf(IOException.class);
}
}
更多高级用法,请参考 github 示例或源码中单元测试。
三、总结
大家在开发时,遇到无法满足的场景时,优先寻找是否有前人已经很好地解决了该问题。
一方面可以验证自己的想法是否靠谱。
另外一方面,即使对方没有很好地解决,也可以参考他人的思路,自己再进行改进。
你是否有更好的方法,欢迎留言和我讨论。
创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。