Jon*_*Jon 177 java logging junit assert
我有一些测试代码,它调用Java记录器来报告其状态.在JUnit测试代码中,我想验证在此记录器中是否输入了正确的日志.以下内容:
methodUnderTest(bool x){
if(x)
logger.info("x happened")
}
@Test tester(){
// perhaps setup a logger first.
methodUnderTest(true);
assertXXXXXX(loggedLevel(),Level.INFO);
}
Run Code Online (Sandbox Code Playgroud)
我想这可以通过特殊改编的记录器(或处理程序或格式化程序)来完成,但我更愿意重用已经存在的解决方案.(而且,说实话,我不清楚如何从记录器获取logRecord,但假设这是可能的.)
Ron*_*hke 137
我也需要这几次.我在下面放了一个小样本,您需要根据自己的需要进行调整.基本上,您可以创建自己的Appender并将其添加到所需的记录器中.如果您想要收集所有内容,则根记录器是一个很好的起点,但如果您愿意,可以使用更具体的记录器.完成后不要忘记删除Appender,否则可能会造成内存泄漏.下面,我已经做了测试范围内,但setUp还是@Before与tearDown或@After可能是更好的地方,根据您的需要.
此外,下面的实现收集List内存中的所有内容.如果你正在记录很多,你可以考虑添加一个过滤器来删除无聊的条目,或者将日志写入磁盘上的临时文件(提示:LoggingEvent是的Serializable,所以你应该能够序列化事件对象,如果你的日志消息是.)
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
public class MyTest {
@Test
public void test() {
final TestAppender appender = new TestAppender();
final Logger logger = Logger.getRootLogger();
logger.addAppender(appender);
try {
Logger.getLogger(MyTest.class).info("Test");
}
finally {
logger.removeAppender(appender);
}
final List<LoggingEvent> log = appender.getLog();
final LoggingEvent firstLogEntry = log.get(0);
assertThat(firstLogEntry.getLevel(), is(Level.INFO));
assertThat((String) firstLogEntry.getMessage(), is("Test"));
assertThat(firstLogEntry.getLoggerName(), is("MyTest"));
}
}
class TestAppender extends AppenderSkeleton {
private final List<LoggingEvent> log = new ArrayList<LoggingEvent>();
@Override
public boolean requiresLayout() {
return false;
}
@Override
protected void append(final LoggingEvent loggingEvent) {
log.add(loggingEvent);
}
@Override
public void close() {
}
public List<LoggingEvent> getLog() {
return new ArrayList<LoggingEvent>(log);
}
}
Run Code Online (Sandbox Code Playgroud)
dav*_*xxx 36
这是一个简单而有效的Logback解决方案.
它不需要添加/创建任何新类.
它依赖于ListAppender:一个whitebox logback appender,其中日志条目被添加public List到我们可以用来做出断言的字段中.
这是一个简单的例子.
Foo类:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Foo {
static final Logger LOGGER = LoggerFactory.getLogger(Foo .class);
public void doThat() {
logger.info("start");
//...
logger.info("finish");
}
}
Run Code Online (Sandbox Code Playgroud)
FooTest类:
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
public class FooTest {
@Test
void doThat() throws Exception {
// get Logback Logger
Logger fooLogger = (Logger) LoggerFactory.getLogger(Foo.class);
// create and start a ListAppender
ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
listAppender.start();
// add the appender to the logger
// addAppender is outdated now
fooLogger.addAppender(listAppender);
// call method under test
Foo foo = new Foo();
foo.doThat();
// JUnit assertions
List<ILoggingEvent> logsList = listAppender.list;
assertEquals("start", logsList.get(0)
.getMessage());
assertEquals(Level.INFO, logsList.get(0)
.getLevel());
assertEquals("finish", logsList.get(1)
.getMessage());
assertEquals(Level.INFO, logsList.get(1)
.getLevel());
}
}
Run Code Online (Sandbox Code Playgroud)
JUnit断言听起来不太适合断言列表元素的某些特定属性.
AscherJ或Hamcrest的匹配/断言库看起来更好:
使用AssertJ,它将是:
import org.assertj.core.api.Assertions;
Assertions.assertThat(listAppender.list)
.extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel)
.containsExactly(Tuple.tuple("start", Level.INFO), Tuple.tuple("finish", Level.INFO));
Run Code Online (Sandbox Code Playgroud)
Jon*_*Jon 34
非常感谢这些(令人惊讶的)快速而有用的答案; 他们让我以正确的方式解决问题.
代码库是我想要使用它,使用java.util.logging作为其记录器机制,我觉得在这些代码中没有足够的东西完全改变它到log4j或记录器接口/外观.但基于这些建议,我"抨击"一个julhandler扩展,并作为一种享受.
简短摘要如下.延伸java.util.logging.Handler:
class LogHandler extends Handler
{
Level lastLevel = Level.FINEST;
public Level checkLevel() {
return lastLevel;
}
public void publish(LogRecord record) {
lastLevel = record.getLevel();
}
public void close(){}
public void flush(){}
}
Run Code Online (Sandbox Code Playgroud)
显然,您可以根据需要存储/想要/需要LogRecord,或者将它们全部压入堆栈,直到溢出为止.
在准备junit-test时,你创建一个java.util.logging.Logger并添加一个新的LogHandler:
@Test tester() {
Logger logger = Logger.getLogger("my junit-test logger");
LogHandler handler = new LogHandler();
handler.setLevel(Level.ALL);
logger.setUseParentHandlers(false);
logger.addHandler(handler);
logger.setLevel(Level.ALL);
Run Code Online (Sandbox Code Playgroud)
调用setUseParentHandlers()是为了使正常处理程序静音,以便(对于此junit-test运行)不会发生不必要的日志记录.无论您的测试代码需要使用此记录器,运行测试和断言均衡:
libraryUnderTest.setLogger(logger);
methodUnderTest(true); // see original question.
assertEquals("Log level as expected?", Level.INFO, handler.checkLevel() );
}
Run Code Online (Sandbox Code Playgroud)
(当然,你会将这项工作的大部分内容转移到一个@Before方法中并进行各种其他改进,但这会使这个演示文稿变得混乱.)
aem*_*aem 17
对于 Junit 5 (Jupiter) Spring 的OutputCaptureExtension非常有用。它从 Spring Boot 2.2 开始可用,并且在spring-boot-test工件中可用。
示例(取自 javadoc):
@ExtendWith(OutputCaptureExtension.class)
class MyTest {
@Test
void test(CapturedOutput output) {
System.out.println("ok");
assertThat(output).contains("ok");
System.err.println("error");
}
@AfterEach
void after(CapturedOutput output) {
assertThat(output.getOut()).contains("ok");
assertThat(output.getErr()).contains("error");
}
}
Run Code Online (Sandbox Code Playgroud)
djn*_*jna 15
实际上,您正在测试依赖类的副作用.对于单元测试,您只需要验证
logger.info()
用正确的参数调用.因此,使用模拟框架来模拟记录器,这将允许您测试自己的类的行为.
Mar*_*cin 15
另一种选择是模拟Appender并验证是否已将消息记录到此appender.Log4j 1.2.x和mockito的示例:
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
public class MyTest {
private final Appender appender = mock(Appender.class);
private final Logger logger = Logger.getRootLogger();
@Before
public void setup() {
logger.addAppender(appender);
}
@Test
public void test() {
// when
Logger.getLogger(MyTest.class).info("Test");
// then
ArgumentCaptor<LoggingEvent> argument = ArgumentCaptor.forClass(LoggingEvent.class);
verify(appender).doAppend(argument.capture());
assertEquals(Level.INFO, argument.getValue().getLevel());
assertEquals("Test", argument.getValue().getMessage());
assertEquals("MyTest", argument.getValue().getLoggerName());
}
@After
public void cleanup() {
logger.removeAppender(appender);
}
}
Run Code Online (Sandbox Code Playgroud)
Boz*_*zho 10
模拟是一个选项,虽然它很难,因为记录器通常是私有静态最终 - 因此设置模拟记录器不会是小菜一碟,或者需要修改被测试的类.
您可以创建自定义Appender(或其任何名称)并注册它 - 通过仅测试配置文件或运行时(在某种程度上,取决于日志记录框架).然后你可以得到那个appender(静态地,如果在配置文件中声明,或者通过它的当前引用,如果你正在插入运行时),并验证它的内容.
对于 log4j2,解决方案略有不同,因为 AppenderSkeleton 不再可用。此外,如果您期望多个日志消息,则使用 Mockito 或类似库创建带有 ArgumentCaptor 的 Appender 将不起作用,因为 MutableLogEvent 在多个日志消息上重用。我为 log4j2 找到的最佳解决方案是:
private static MockedAppender mockedAppender;
private static Logger logger;
@Before
public void setup() {
mockedAppender.message.clear();
}
/**
* For some reason mvn test will not work if this is @Before, but in eclipse it works! As a
* result, we use @BeforeClass.
*/
@BeforeClass
public static void setupClass() {
mockedAppender = new MockedAppender();
logger = (Logger)LogManager.getLogger(MatchingMetricsLogger.class);
logger.addAppender(mockedAppender);
logger.setLevel(Level.INFO);
}
@AfterClass
public static void teardown() {
logger.removeAppender(mockedAppender);
}
@Test
public void test() {
// do something that causes logs
for (String e : mockedAppender.message) {
// add asserts for the log messages
}
}
private static class MockedAppender extends AbstractAppender {
List<String> message = new ArrayList<>();
protected MockedAppender() {
super("MockedAppender", null, null);
}
@Override
public void append(LogEvent event) {
message.add(event.getMessage().getFormattedMessage());
}
}
Run Code Online (Sandbox Code Playgroud)
受@ RonaldBlaschke解决方案的启发,我想到了这个:
public class Log4JTester extends ExternalResource {
TestAppender appender;
@Override
protected void before() {
appender = new TestAppender();
final Logger rootLogger = Logger.getRootLogger();
rootLogger.addAppender(appender);
}
@Override
protected void after() {
final Logger rootLogger = Logger.getRootLogger();
rootLogger.removeAppender(appender);
}
public void assertLogged(Matcher<String> matcher) {
for(LoggingEvent event : appender.events) {
if(matcher.matches(event.getMessage())) {
return;
}
}
fail("No event matches " + matcher);
}
private static class TestAppender extends AppenderSkeleton {
List<LoggingEvent> events = new ArrayList<LoggingEvent>();
@Override
protected void append(LoggingEvent event) {
events.add(event);
}
@Override
public void close() {
}
@Override
public boolean requiresLayout() {
return false;
}
}
}
Run Code Online (Sandbox Code Playgroud)
...允许你这样做:
@Rule public Log4JTester logTest = new Log4JTester();
@Test
public void testFoo() {
user.setStatus(Status.PREMIUM);
logTest.assertLogged(
stringContains("Note added to account: premium customer"));
}
Run Code Online (Sandbox Code Playgroud)
你可能会以更聪明的方式使用hamcrest,但我已经把它留在了这里.
哇。我不确定为什么这这么难。我发现我无法使用上面的任何代码示例,因为我在 slf4j 上使用了 log4j2。这是我的解决方案:
public class SpecialLogServiceTest {
@Mock
private Appender appender;
@Captor
private ArgumentCaptor<LogEvent> captor;
@InjectMocks
private SpecialLogService specialLogService;
private LoggerConfig loggerConfig;
@Before
public void setUp() {
// prepare the appender so Log4j likes it
when(appender.getName()).thenReturn("MockAppender");
when(appender.isStarted()).thenReturn(true);
when(appender.isStopped()).thenReturn(false);
final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
final Configuration config = ctx.getConfiguration();
loggerConfig = config.getLoggerConfig("org.example.SpecialLogService");
loggerConfig.addAppender(appender, AuditLogCRUDService.LEVEL_AUDIT, null);
}
@After
public void tearDown() {
loggerConfig.removeAppender("MockAppender");
}
@Test
public void writeLog_shouldCreateCorrectLogMessage() throws Exception {
SpecialLog specialLog = new SpecialLogBuilder().build();
String expectedLog = "this is my log message";
specialLogService.writeLog(specialLog);
verify(appender).append(captor.capture());
assertThat(captor.getAllValues().size(), is(1));
assertThat(captor.getAllValues().get(0).getMessage().toString(), is(expectedLog));
}
}
Run Code Online (Sandbox Code Playgroud)
小智 6
最简单的方法
@ExtendWith(OutputCaptureExtension.class)
class MyTestClass {
@Test
void my_test_method(CapturedOutput output) {
assertThat(output).contains("my test log.");
}
}
Run Code Online (Sandbox Code Playgroud)
正如其他人提到的,您可以使用模拟框架。为此,您必须在类中公开记录器(尽管我可能更喜欢将其设置为私有而不是创建公共设置器)。
另一种解决方案是手动创建一个假记录器。您必须编写伪造的记录器(更多夹具代码),但在这种情况下,我更喜欢针对模拟框架中保存的代码增强测试的可读性。
我会做这样的事情:
class FakeLogger implements ILogger {
public List<String> infos = new ArrayList<String>();
public List<String> errors = new ArrayList<String>();
public void info(String message) {
infos.add(message);
}
public void error(String message) {
errors.add(message);
}
}
class TestMyClass {
private MyClass myClass;
private FakeLogger logger;
@Before
public void setUp() throws Exception {
myClass = new MyClass();
logger = new FakeLogger();
myClass.logger = logger;
}
@Test
public void testMyMethod() {
myClass.myMethod(true);
assertEquals(1, logger.infos.size());
}
}
Run Code Online (Sandbox Code Playgroud)
这是我为 logback 所做的。
我创建了一个 TestAppender 类:
public class TestAppender extends AppenderBase<ILoggingEvent> {
private Stack<ILoggingEvent> events = new Stack<ILoggingEvent>();
@Override
protected void append(ILoggingEvent event) {
events.add(event);
}
public void clear() {
events.clear();
}
public ILoggingEvent getLastEvent() {
return events.pop();
}
}
Run Code Online (Sandbox Code Playgroud)
然后在我的 testng 单元测试类的父级中,我创建了一个方法:
protected TestAppender testAppender;
@BeforeClass
public void setupLogsForTesting() {
Logger root = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
testAppender = (TestAppender)root.getAppender("TEST");
if (testAppender != null) {
testAppender.clear();
}
}
Run Code Online (Sandbox Code Playgroud)
我在 src/test/resources 中定义了一个 logback-test.xml 文件,并添加了一个测试 appender:
<appender name="TEST" class="com.intuit.icn.TestAppender">
<encoder>
<pattern>%m%n</pattern>
</encoder>
</appender>
Run Code Online (Sandbox Code Playgroud)
并将这个 appender 添加到根 appender:
<root>
<level value="error" />
<appender-ref ref="STDOUT" />
<appender-ref ref="TEST" />
</root>
Run Code Online (Sandbox Code Playgroud)
现在,在从父测试类扩展的测试类中,我可以获取 appender 并记录最后一条消息并验证消息、级别、可抛出。
ILoggingEvent lastEvent = testAppender.getLastEvent();
assertEquals(lastEvent.getMessage(), "...");
assertEquals(lastEvent.getLevel(), Level.WARN);
assertEquals(lastEvent.getThrowableProxy().getMessage(), "...");
Run Code Online (Sandbox Code Playgroud)
我也遇到了同样的挑战并最终到达了这个页面。虽然我回答这个问题已经晚了 11 年,但我想也许它对其他人仍然有用。我发现davidxxxLogback 和 ListAppander的答案非常有用。我对多个项目使用了相同的配置,但是当我需要更改某些内容时,复制/粘贴它并维护所有版本并不是那么有趣。我认为用它制作一个图书馆并回馈社区会更好。它适用于 SLFJ4、Log4j、Log4j2、Java Util Logging 和 Lombok 注释。请查看此处:LogCaptor以获取详细示例以及如何将其添加到您的项目中。
示例情况:
public class FooService {
private static final Logger LOGGER = LoggerFactory.getLogger(FooService.class);
public void sayHello() {
LOGGER.warn("Congratulations, you are pregnant!");
}
}
Run Code Online (Sandbox Code Playgroud)
使用 LogCaptor 的示例单元测试:
public class FooServiceTest {
private LogCaptor logCaptor = LogCaptor.forClass(FooService.class);
@Test
public void logInfoAndWarnMessages() {
FooService fooService = new FooService();
fooService.sayHello();
assertThat(logCaptor.getWarnLogs())
.contains("Congratulations, you are pregnant!");
}
}
Run Code Online (Sandbox Code Playgroud)
我不太确定我是否应该在这里发布它,因为它也可以被视为一种推广“我的库”的方式,但我认为它可能对面临相同挑战的开发人员有所帮助。
| 归档时间: |
|
| 查看次数: |
150489 次 |
| 最近记录: |