Unit testing 单元测试SLF4J日志消息的最佳方法是什么?
我使用的是slf4j,我想对代码进行单元测试,以确保在特定条件下生成警告/错误日志消息。我希望这些是严格的单元测试,所以我不希望为了测试日志消息是否生成而必须从文件中调出日志配置。我使用的模拟框架是Mockito。您可以将需要测试的重要日志调用放在它们自己的方法中,这样可以更轻松地模拟Unit testing 单元测试SLF4J日志消息的最佳方法是什么?,unit-testing,mocking,mockito,slf4j,Unit Testing,Mocking,Mockito,Slf4j,我使用的是slf4j,我想对代码进行单元测试,以确保在特定条件下生成警告/错误日志消息。我希望这些是严格的单元测试,所以我不希望为了测试日志消息是否生成而必须从文件中调出日志配置。我使用的模拟框架是Mockito。您可以将需要测试的重要日志调用放在它们自己的方法中,这样可以更轻松地模拟 如果你真的想模拟SLF4J,我敢打赌你可以为它创建自己的提供者,这将允许你从SLF4J端提供模拟记录器,而不是在你的服务对象中注入模拟记录器。我认为你可以用自定义附加器解决你的问题。创建一个实现org.apach
如果你真的想模拟SLF4J,我敢打赌你可以为它创建自己的提供者,这将允许你从SLF4J端提供模拟记录器,而不是在你的服务对象中注入模拟记录器。我认为你可以用自定义附加器解决你的问题。创建一个实现
org.apache.log4j.appender
的测试appender,在log4j.properties
中设置appender,并在执行测试用例时加载它
如果您从该
appender
调用测试线束,您可以检查记录的消息类似于@Zsolt,您可以模拟log4jappender
并在记录器上设置它,然后验证对appender.doAppend()的调用。这允许您在不修改实际代码的情况下进行测试。要在不依赖特定实现(如log4j)的情况下测试slf4j,您可以提供自己的slf4j日志记录实现,如中所述。您的实现可以记录记录的消息,然后由您的单元测试询问以进行验证
软件包就是这样做的。它是一个内存中的slf4j日志记录实现,提供了检索日志消息的方法。一个更好的slf4j测试实现,在具有并发测试执行的环境中工作得非常好
我加入了一些关于slf4j日志测试和现有测试方法在并发测试执行方面的局限性的讨论
我决定把我的话写成代码,结果就是git repo。创建一个测试规则:
导入ch.qos.logback.classic.Logger;
导入ch.qos.logback.classic.spi.ILoggingEvent;
导入ch.qos.logback.core.read.ListAppender;
导入org.junit.rules.TestRule;
导入org.junit.runner.Description;
导入org.junit.runners.model.Statement;
导入org.slf4j.LoggerFactory;
导入java.util.List;
导入java.util.stream.collector;
公共类LoggerRule实现TestRule{
私有最终ListAppender ListAppender=新ListAppender();
私有最终记录器=(记录器)LoggerFactory.getLogger(Logger.ROOT\u Logger\u NAME);
@凌驾
公开声明应用(声明库、说明){
返回新语句(){
@凌驾
public void evaluate()可丢弃{
设置();
base.evaluate();
撕裂();
}
};
}
私有无效设置(){
logger.addAppender(listpappender);
listpappender.start();
}
私有void拆卸(){
listpappender.stop();
listAppender.list.clear();
记录器.拆卸附件器(列表附件器);
}
公共列表getMessages(){
返回listAppender.list.stream().map(e->e.getMessage()).collect(Collectors.toList());
}
公共列表getFormattedMessages(){
返回listAppender.list.stream().map(e->e.getFormattedMessage()).collect(Collectors.toList());
}
}
然后使用它:
@RegisterExtension
public LoggerExtension loggerExtension = new LoggerExtension();
@Test
public void yourTest() {
// ...
assertThat(loggerExtension.getFormattedMessages().size()).isEqualTo(2);
}
@规则
public final LoggerRule LoggerRule=新LoggerRule();
@试验
公共测试(){
// ...
资产(loggerRule.getFormattedMessages().size()).isEqualTo(2);
}
我有一个新答案,我将在这篇文章的顶部发布(我的“旧”答案仍然在这篇文章的底部)(在写我的“旧”答案时是“0”,所以没有伤害,没有犯规!)
更新的答案:
这是Gradle软件包:
testImplementation 'com.portingle:slf4jtesting:1.1.3'
Maven链接:
密切相关代码:
(下面是MyTestClass(.java)中的导入和私有方法)
====================================================下面是旧答案。。不要使用================
下面是我以前的回答。我更改了下面的代码。。。在我发现上面的包后使用它(上面的包)
或与上面类似,但在自定义记录器上执行强制转换
private void myTest(){
记录器lgr=getSimpleLog();
MyColClass testClass=新的MyColClass(lgr);
int myValue=333;
testClass.doSomething(myValue);
String findMessage=String.format(myColClass.PROCESS\u已启动,myValue);
InMemoryUnitsLogger castLogger=(InMemoryUnitsLogger)lgr;
/*现在检查消息的确切子集合)*/
assertTrue(castLogger.getInfos().contains(findMessage));
}
对代码持保留态度,想法就在那里。我没有编译代码。我知道这个问题发布已经有一段时间了,但我刚刚遇到了一个类似的问题,我的解决方案可能会有所帮助。按照@Zsolt提出的解决方案,我们使用了appender,更具体地说是Logback的列表appender
。此处显示代码和配置(Groovy代码,但可以轻松移植到Java):
用于日志访问的Groovy类:
import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.spi.LoggingEvent
import ch.qos.logback.core.read.ListAppender
import org.slf4j.LoggerFactory
class LogAccess {
final static String DEFAULT_PACKAGE_DOMAIN = Logger.ROOT_LOGGER_NAME
final static String DEFAULT_APPENDER_NAME = 'LIST'
final List<LoggingEvent> list
LogAccess(String packageDomain = DEFAULT_PACKAGE_DOMAIN, String appenderName = DEFAULT_APPENDER_NAME) {
Logger logger = (Logger) LoggerFactory.getLogger(packageDomain)
ListAppender<LoggingEvent> appender = logger.getAppender(appenderName) as ListAppender<LoggingEvent>
if (appender == null) {
throw new IllegalStateException("'$DEFAULT_APPENDER_NAME' appender not found. Did you forget to add 'logback.xml' to the resources folder?")
}
this.list = appender.list
this.clear()
}
void clear() {
list.clear()
}
boolean contains(String logMessage) {
return list.reverse().any { it.getFormattedMessage() == logMessage }
}
@Override
String toString() {
list.collect { it. getFormattedMessage() }
}
}
我在带有Groovy+Spock的SpringBoot项目中使用了它,尽管我不明白为什么它在任何带有Logback的Java项目中都不起作用。只需使用普通的Mockito和一些反射逻辑来模拟它:
// Mock the Logger
Logger mock = Mockito.mock(Logger.class);
// Set the Logger to the class you want to test.
// Since this is often a private static field you have to
// hack a little bit: (Solution taken from https://stackoverflow.com/a/3301720/812093)
setFinalStatic(ClassBeeingTested.class.getDeclaredField("log"), mock);
使用setFinalStatic方法Being
public static void setFinalStatic(Field field, Object newValue) throws Exception {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, newValue);
}
然后只需执行待测试代码并进行验证-例如,以下内容验证是否调用了Logger.warn方法两次:
ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
Mockito.verify(mock,Mockito.atLeastOnce()).warn(argumentCaptor.capture());
List<String> allValues = argumentCaptor.getAllValues();
assertEquals(2, allValues.size());
assertEquals("myFirstExpectedMessage", allValues.get(0));
assertEquals("mySecondExpectedMessage", allValues.get(1));
ArgumentCaptor ArgumentCaptor=ArgumentCaptor.forClass(String.class);
验证(mock,Mockito.atLeastOnce()).warn(argumentCaptor.capture(
LogAccess log = new LogAccess()
def expectedLogEntry = 'Expected Log Entry'
assert !log.contains(expectedLogEntry)
methodUnderTest()
assert log.contains(expectedLogEntry)
// Mock the Logger
Logger mock = Mockito.mock(Logger.class);
// Set the Logger to the class you want to test.
// Since this is often a private static field you have to
// hack a little bit: (Solution taken from https://stackoverflow.com/a/3301720/812093)
setFinalStatic(ClassBeeingTested.class.getDeclaredField("log"), mock);
public static void setFinalStatic(Field field, Object newValue) throws Exception {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, newValue);
}
ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
Mockito.verify(mock,Mockito.atLeastOnce()).warn(argumentCaptor.capture());
List<String> allValues = argumentCaptor.getAllValues();
assertEquals(2, allValues.size());
assertEquals("myFirstExpectedMessage", allValues.get(0));
assertEquals("mySecondExpectedMessage", allValues.get(1));
<dependency>
<groupId>uk.org.lidalia</groupId>
<artifactId>slf4j-test</artifactId>
<version>1.2.0</version>
</dependency>
@Slf4j
public class SampleClass {
public void logDetails(){
log.info("Logging");
}
}
import org.junit.Test;
import uk.org.lidalia.slf4jtest.TestLogger;
import uk.org.lidalia.slf4jtest.TestLoggerFactory;
import static java.util.Arrays.asList;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static uk.org.lidalia.slf4jtest.LoggingEvent.info;
public class SampleClassTest {
TestLogger logger = TestLoggerFactory.getTestLogger(SampleClass.class);
@Test
public void testLogging(){
SampleClass sampleClass = new SampleClass();
//Invoke slf4j logger
sampleClass.logDetails();
assertThat(logger.getLoggingEvents(), is(asList(info("Logging"))));
}
}
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.slf4j.Logger;
@RunWith(MockitoJUnitRunner.class)
public class JUnit4ExampleTest {
private static final String INFO_TEST_MESSAGE = "info log test message from JUnit4";
@Mock
Logger logger;
@InjectMocks
Example sut;
@Test
public void logInfoShouldBeLogged() {
// when
sut.methodWithLogInfo(INFO_TEST_MESSAGE);
// then
Mockito.verify(logger).info(INFO_TEST_MESSAGE);
Mockito.verifyNoMoreInteractions(logger);
}
}
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.stream.Collectors;
public class LoggerExtension implements BeforeEachCallback, AfterEachCallback {
private final ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
private final Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
@Override
public void afterEach(ExtensionContext extensionContext) throws Exception {
listAppender.stop();
listAppender.list.clear();
logger.detachAppender(listAppender);
}
@Override
public void beforeEach(ExtensionContext extensionContext) throws Exception {
logger.addAppender(listAppender);
listAppender.start();
}
public List<String> getMessages() {
return listAppender.list.stream().map(e -> e.getMessage()).collect(Collectors.toList());
}
public List<String> getFormattedMessages() {
return listAppender.list.stream().map(e -> e.getFormattedMessage()).collect(Collectors.toList());
}
}
@RegisterExtension
public LoggerExtension loggerExtension = new LoggerExtension();
@Test
public void yourTest() {
// ...
assertThat(loggerExtension.getFormattedMessages().size()).isEqualTo(2);
}