Java 如何对记录器中的消息进行 JUnit 断言
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/1827677/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to do a JUnit assert on a message in a logger
提问by Jon
I have some code-under-test that calls on a Java logger to report its status. In the JUnit test code, I would like to verify that the correct log entry was made in this logger. Something along the following lines:
我有一些被测代码调用 Java 记录器来报告其状态。在 JUnit 测试代码中,我想验证此记录器中是否创建了正确的日志条目。大致如下:
methodUnderTest(bool x){
if(x)
logger.info("x happened")
}
@Test tester(){
// perhaps setup a logger first.
methodUnderTest(true);
assertXXXXXX(loggedLevel(),Level.INFO);
}
I suppose that this could be done with a specially adapted logger (or handler, or formatter), but I would prefer to re-use a solution that already exists. (And, to be honest, it is not clear to me how to get at the logRecord from a logger, but suppose that that's possible.)
我想这可以使用经过特殊调整的记录器(或处理程序或格式化程序)来完成,但我更愿意重新使用已经存在的解决方案。(而且,说实话,我不清楚如何从记录器获取 logRecord,但假设这是可能的。)
采纳答案by Jon
Thanks a lot for these (surprisingly) quick and helpful answers; they put me on the right way for my solution.
非常感谢这些(令人惊讶的)快速且有用的答案;他们让我走上正确的道路来解决我的问题。
The codebase were I want to use this, uses java.util.logging as its logger mechanism, and I don't feel at home enough in those codes to completely change that to log4j or to logger interfaces/facades. But based on these suggestions, I 'hacked-up' a j.u.l.handler extension and that works as a treat.
代码库是我想使用它,使用 java.util.logging 作为它的记录器机制,而且我对这些代码感觉不够自如,无法将其完全更改为 log4j 或记录器接口/外观。但是基于这些建议,我“修改”了一个 julhandler 扩展,这可以作为一种享受。
A short summary follows. Extend java.util.logging.Handler
:
下面是一个简短的总结。扩展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(){}
}
Obviously, you can store as much as you like/want/need from the LogRecord
, or push them all into a stack until you get an overflow.
显然,您可以从 中存储您喜欢/想要/需要的数量LogRecord
,或者将它们全部推入堆栈直到溢出。
In the preparation for the junit-test, you create a java.util.logging.Logger
and add such a new LogHandler
to it:
在准备 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);
The call to setUseParentHandlers()
is to silence the normal handlers, so that (for this junit-test run) no unnecessary logging happens. Do whatever your code-under-test needs to use this logger, run the test and assertEquality:
调用setUseParentHandlers()
是使正常处理程序静音,以便(对于此 junit-test 运行)不会发生不必要的日志记录。做任何你的被测代码需要使用这个记录器,运行测试和 assertEquality:
libraryUnderTest.setLogger(logger);
methodUnderTest(true); // see original question.
assertEquals("Log level as expected?", Level.INFO, handler.checkLevel() );
}
(Of course, you would move large part of this work into a @Before
method and make assorted other improvements, but that would clutter this presentation.)
(当然,您会将这项工作的很大一部分移到一种@Before
方法中并进行各种其他改进,但这会使本演示文稿变得混乱。)
回答by djna
Effectively you are testing a side-effect of a dependent class. For unit testing you need only to verify that
实际上,您正在测试依赖类的副作用。对于单元测试,您只需要验证
logger.info()
logger.info()
was called with the correct parameter. Hence use a mocking framework to emulate logger and that will allow you to test your own class's behaviour.
使用正确的参数调用。因此,使用模拟框架来模拟记录器,这将允许您测试自己的类的行为。
回答by Bozho
Mocking is an option here, although it would be hard, because loggers are generally private static final - so setting a mock logger wouldn't be a piece of cake, or would require modification of the class under test.
Mocking 是这里的一个选项,虽然它会很难,因为记录器通常是私有的 static final - 所以设置一个模拟记录器不是小菜一碟,或者需要修改被测类。
You can create a custom Appender (or whatever it's called), and register it - either via a test-only configuration file, or runtime (in a way, dependent on the logging framework). And then you can get that appender (either statically, if declared in configuration file, or by its current reference, if you are plugging it runtime), and verify its contents.
你可以创建一个自定义的 Appender(或任何它叫的名字),并注册它——要么通过仅测试的配置文件,要么通过运行时(在某种程度上,依赖于日志框架)。然后你可以得到那个 appender(静态的,如果在配置文件中声明,或者通过它的当前引用,如果你在运行时插入它),并验证它的内容。
回答by Arne Deutsch
As mentioned from the others you could use a mocking framework. For this to make work you have to expose the logger in your class (although I would propably prefere to make it package private instead of creating a public setter).
正如其他人提到的,您可以使用模拟框架。为此,您必须在类中公开记录器(尽管我可能更喜欢将其设置为私有而不是创建公共设置器)。
The other solution is to create a fake logger by hand. You have to write the fake logger (more fixture code) but in this case I would prefer the enhanced readability of the tests against the saved code from the mocking framework.
另一种解决方案是手动创建一个假记录器。您必须编写假记录器(更多夹具代码),但在这种情况下,我更喜欢针对模拟框架中保存的代码增强测试的可读性。
I would do something like this:
我会做这样的事情:
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());
}
}
回答by Ronald Blaschke
I've needed this several times as well. I've put together a small sample below, which you'd want to adjust to your needs. Basically, you create your own Appender
and add it to the logger you want. If you'd want to collect everything, the root logger is a good place to start, but you can use a more specific if you'd like. Don't forget to remove the Appender when you're done, otherwise you might create a memory leak. Below I've done it within the test, but setUp
or @Before
and tearDown
or @After
might be better places, depending on your needs.
我也多次需要这个。我在下面汇总了一个小样本,您可以根据自己的需要对其进行调整。基本上,您可以创建自己Appender
的记录器并将其添加到您想要的记录器中。如果您想收集所有内容,根记录器是一个不错的起点,但如果您愿意,可以使用更具体的。完成后不要忘记删除 Appender,否则可能会造成内存泄漏。下面我在测试中完成了它,但是setUp
or@Before
和tearDown
or@After
可能是更好的地方,具体取决于您的需要。
Also, the implementation below collects everything in a List
in memory. If you're logging a lot you might consider adding a filter to drop boring entries, or to write the log to a temporary file on disk (Hint: LoggingEvent
is Serializable
, so you should be able to just serialize the event objects, if your log message is.)
此外,下面的实现收集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);
}
}
回答by kfox
Here is what i did for logback.
这是我为 logback 所做的。
I created a TestAppender class:
我创建了一个 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();
}
}
Then in the parent of my testng unit test class I created a method:
然后在我的 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();
}
}
I have a logback-test.xml file defined in src/test/resources and I added a test appender:
我在 src/test/resources 中定义了一个 logback-test.xml 文件,并添加了一个测试 appender:
<appender name="TEST" class="com.intuit.icn.TestAppender">
<encoder>
<pattern>%m%n</pattern>
</encoder>
</appender>
and added this appender to the root appender:
并将这个 appender 添加到根 appender:
<root>
<level value="error" />
<appender-ref ref="STDOUT" />
<appender-ref ref="TEST" />
</root>
Now in my test classes that extend from my parent test class I can get the appender and get the last message logged and verify the message, the level, the throwable.
现在,在从父测试类扩展的测试类中,我可以获取 appender 并记录最后一条消息并验证消息、级别、可抛出。
ILoggingEvent lastEvent = testAppender.getLastEvent();
assertEquals(lastEvent.getMessage(), "...");
assertEquals(lastEvent.getLevel(), Level.WARN);
assertEquals(lastEvent.getThrowableProxy().getMessage(), "...");
回答by Marcin
Another option is to mock Appender and verify if message was logged to this appender. Example for Log4j 1.2.x and mockito:
另一种选择是模拟 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);
}
}
回答by GregD
Another idea worth mentioning, although it's an older topic, is creating a CDI producer to inject your logger so the mocking becomes easy. (And it also gives the advantage of not having to declare the "whole logger statement" anymore, but that's off-topic)
另一个值得一提的想法,虽然它是一个较旧的主题,但它是创建一个 CDI 生成器来注入您的记录器,以便模拟变得容易。(而且它还提供了不必再声明“整个记录器语句”的优点,但这是题外话)
Example:
例子:
Creating the logger to inject:
创建要注入的记录器:
public class CdiResources {
@Produces @LoggerType
public Logger createLogger(final InjectionPoint ip) {
return Logger.getLogger(ip.getMember().getDeclaringClass());
}
}
The qualifier:
预选赛:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface LoggerType {
}
Using the logger in your production code:
在您的生产代码中使用记录器:
public class ProductionCode {
@Inject
@LoggerType
private Logger logger;
public void logSomething() {
logger.info("something");
}
}
Testing the logger in your test code (giving an easyMock example):
在您的测试代码中测试记录器(给出一个 easyMock 示例):
@TestSubject
private ProductionCode productionCode = new ProductionCode();
@Mock
private Logger logger;
@Test
public void testTheLogger() {
logger.info("something");
replayAll();
productionCode.logSomething();
}
回答by slim
Inspired by @RonaldBlaschke's solution, I came up with this:
受到@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;
}
}
}
... which allows you to do:
...这允许您执行以下操作:
@Rule public Log4JTester logTest = new Log4JTester();
@Test
public void testFoo() {
user.setStatus(Status.PREMIUM);
logTest.assertLogged(
stringContains("Note added to account: premium customer"));
}
You could probably make it use hamcrest in a smarter way, but I've left it at this.
你可能可以让它以更聪明的方式使用 hamcrest,但我已经把它留在了这里。
回答by Yarix
Using Jmockit (1.21) I was able to write this simple test. The test makes sure a specific ERROR message is called just once.
使用 Jmockit (1.21) 我能够编写这个简单的测试。该测试确保只调用一次特定的 ERROR 消息。
@Test
public void testErrorMessage() {
final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger( MyConfig.class );
new Expectations(logger) {{
//make sure this error is happens just once.
logger.error( "Something went wrong..." );
times = 1;
}};
new MyTestObject().runSomethingWrong( "aaa" ); //SUT that eventually cause the error in the log.
}