簡體   English   中英

如何對記錄器中的消息執行 JUnit 斷言

[英]How to do a JUnit assert on a message in a logger

我有一些被測代碼調用 Java 記錄器來報告其狀態。 在 JUnit 測試代碼中,我想驗證是否在此記錄器中創建了正確的日志條目。 大致如下:

methodUnderTest(bool x){
    if(x)
        logger.info("x happened")
}

@Test tester(){
    // perhaps setup a logger first.
    methodUnderTest(true);
    assertXXXXXX(loggedLevel(),Level.INFO);
}

我想這可以用一個特別適應的記錄器(或處理程序,或格式化程序)來完成,但我更願意重新使用已經存在的解決方案。 (老實說,我不清楚如何從記錄器獲取 logRecord,但假設這是可能的。)

我也需要這個好幾次。 我在下面匯總了一個小樣本,您可以根據自己的需要進行調整。 基本上,您創建自己的Appender並將其添加到所需的記錄器中。 如果您想收集所有內容,根記錄器是一個不錯的起點,但如果您願意,可以使用更具體的記錄器。 完成后不要忘記刪除 Appender,否則可能會造成內存泄漏。 下面我在測試中完成了它,但根據您的需要, setUp@BeforetearDown@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);
    }
}

這是一個簡單高效的 Logback 解決方案。
它不需要添加/創建任何新類。
它依賴於ListAppender :一個白盒 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");
    }
}

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());
    }
}

JUnit 斷言聽起來不太適合斷言列表元素的某些特定屬性。
匹配器/斷言庫如 AssertJ 或 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));

非常感謝這些(令人驚訝的)快速而有用的答案; 他們讓我走上了解決問題的正確道路。

代碼庫是我想使用它,使用 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(){}
}

顯然,您可以從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);

setUseParentHandlers()的調用是為了使普通處理程序靜音,以便(對於此 junit-test 運行)不會發生不必要的日志記錄。 做任何你的被測代碼需要使用這個記錄器,運行測試和 assertEquality:

    libraryUnderTest.setLogger(logger);
    methodUnderTest(true);  // see original question.
    assertEquals("Log level as expected?", Level.INFO, handler.checkLevel() );
}

(當然,您會將大部分工作轉移到@Before方法中並進行各種其他改進,但這會使演示文稿變得混亂。)

對於 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");
    }
}

實際上,您正在測試依賴類的副作用。 對於單元測試,您只需要驗證

logger.info()

使用正確的參數調用。 因此使用模擬框架來模擬記錄器,這將允許您測試自己的類的行為。

另一種選擇是模擬 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);
    }
}

我也遇到了同樣的挑戰,最終出現在這個頁面上。 盡管我回答這個問題已經晚了 11 年,但我想也許它對其他人仍然有用。 我發現使用 Logback 和 ListAppander 的davidxxx的答案非常有用。 我對多個項目使用了相同的配置,但是當我需要更改某些內容時,復制/粘貼它並維護所有版本並不是那么有趣。 我認為用它做一個圖書館並回饋社區會更好。 它適用於 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!");
    }

}

使用 LogCaptor 的示例單元測試:

import nl.altindag.log.LogCaptor;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class FooServiceTest {

    @Test
    public void sayHelloShouldLogWarnMessage() {
        LogCaptor logCaptor = LogCaptor.forClass(FooService.class);

        FooService fooService = new FooService();
        fooService.sayHello();

        assertThat(logCaptor.getWarnLogs())
            .contains("Congratulations, you are pregnant!");
    }
}

我不太確定是否應該在此處發布此內容,因為它也可以被視為宣傳“我的庫”的一種方式,但我認為這對面臨同樣挑戰的開發人員可能會有所幫助。

模擬是這里的一個選項,雖然這很難,因為記錄器通常是私有的靜態最終 - 所以設置模擬記錄器不是小菜一碟,或者需要修改被測類。

您可以創建一個自定義 Appender(或任何它被稱為的),並注冊它 - 通過僅測試配置文件或運行時(在某種程度上,取決於日志記錄框架)。 然后您可以獲取該附加程序(靜態,如果在配置文件中聲明,或者通過其當前引用,如果您在運行時插入它),並驗證其內容。

受@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;
        }
    }

}

...它允許您執行以下操作:

@Rule public Log4JTester logTest = new Log4JTester();

@Test
public void testFoo() {
     user.setStatus(Status.PREMIUM);
     logTest.assertLogged(
        stringContains("Note added to account: premium customer"));
}

你可能可以讓它以更聰明的方式使用 hamcrest,但我已經把它留在了這里。

對於 log4j2,解決方案略有不同,因為 AppenderSkeleton 不再可用。 此外,如果您期望多條日志消息,則使用 Mockito 或類似庫來創建帶有 ArgumentCaptor 的 Appender 將不起作用,因為 MutableLogEvent 在多條日志消息上被重用。 我為 log4j2 找到的最佳解決方案是:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.appender.AbstractAppender;

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());
    }
}

哇。 我不確定這為什么這么難。 我發現我無法使用上面的任何代碼示例,因為我使用的是 log4j2 而不是 slf4j。 這是我的解決方案:

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));
  }
}

正如其他人提到的,您可以使用模擬框架。 為此,您必須在您的班級中公開記錄器(盡管我可能更願意將其設為私有而不是創建公共設置器)。

另一種解決方案是手動創建一個假記錄器。 您必須編寫假記錄器(更多固定代碼),但在這種情況下,我希望針對模擬框架中保存的代碼增強測試的可讀性。

我會做這樣的事情:

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());
    }
}

這是我為 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();
    }
}

然后在我的 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();
    }
}

我在 src/test/resources 中定義了一個 logback-test.xml 文件,並添加了一個測試附加程序:

<appender name="TEST" class="com.intuit.icn.TestAppender">
    <encoder>
        <pattern>%m%n</pattern>
    </encoder>
</appender>

並將此附加程序添加到根附加程序:

<root>
    <level value="error" />
    <appender-ref ref="STDOUT" />
    <appender-ref ref="TEST" />
</root>

現在,在從父測試類擴展的測試類中,我可以獲取附加程序並記錄最后一條消息並驗證消息、級別、可拋出對象。

ILoggingEvent lastEvent = testAppender.getLastEvent();
assertEquals(lastEvent.getMessage(), "...");
assertEquals(lastEvent.getLevel(), Level.WARN);
assertEquals(lastEvent.getThrowableProxy().getMessage(), "...");
Here is the sample code to mock log, irrespective of the version used for junit or sping, springboot.

import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.Appender;
import org.mockito.ArgumentMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.Test;

import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class MyTest {
  private static Logger logger = LoggerFactory.getLogger(MyTest.class);

    @Test
    public void testSomething() {
    ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
    final Appender mockAppender = mock(Appender.class);
    when(mockAppender.getName()).thenReturn("MOCK");
    root.addAppender(mockAppender);

    //... do whatever you need to trigger the log

    verify(mockAppender).doAppend(argThat(new ArgumentMatcher() {
      @Override
      public boolean matches(final Object argument) {
        return ((LoggingEvent)argument).getFormattedMessage().contains("Hey this is the message I want to see");
      }
    }));
  }
}

請注意,在 Log4J 2.x 中,公共接口org.apache.logging.log4j.Logger不包括setAppender()removeAppender()方法。

但是,如果您沒有做任何花哨的事情,您應該能夠將其轉換為實現類org.apache.logging.log4j.core.Logger ,它確實公開了這些方法。

這是MockitoAssertJ的示例:

// Import the implementation class rather than the API interface
import org.apache.logging.log4j.core.Logger;
// Cast logger to implementation class to get access to setAppender/removeAppender
Logger log = (Logger) LogManager.getLogger(MyClassUnderTest.class);

// Set up the mock appender, stubbing some methods Log4J needs internally
Appender appender = mock(Appender.class);
when(appender.getName()).thenReturn("Mock Appender");
when(appender.isStarted()).thenReturn(true);

log.addAppender(appender);
try {
    new MyClassUnderTest().doSomethingThatShouldLogAnError();
} finally {
    log.removeAppender(appender);
}

// Verify that we got an error with the expected message
ArgumentCaptor<LogEvent> logEventCaptor = ArgumentCaptor.forClass(LogEvent.class);
verify(appender).append(logEventCaptor.capture());
LogEvent logEvent = logEventCaptor.getValue();
assertThat(logEvent.getLevel()).isEqualTo(Level.ERROR);
assertThat(logEvent.getMessage().getFormattedMessage()).contains(expectedErrorMessage);

至於我,您可以通過將JUnitMockito一起使用來簡化您的測試。 我為此提出以下解決方案:

import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.Mockito.times;

@RunWith(MockitoJUnitRunner.class)
public class MyLogTest {
    private static final String FIRST_MESSAGE = "First message";
    private static final String SECOND_MESSAGE = "Second message";
    @Mock private Appender appender;
    @Captor private ArgumentCaptor<LoggingEvent> captor;
    @InjectMocks private MyLog;

    @Before
    public void setUp() {
        LogManager.getRootLogger().addAppender(appender);
    }

    @After
    public void tearDown() {
        LogManager.getRootLogger().removeAppender(appender);
    }

    @Test
    public void shouldLogExactlyTwoMessages() {
        testedClass.foo();

        then(appender).should(times(2)).doAppend(captor.capture());
        List<LoggingEvent> loggingEvents = captor.getAllValues();
        assertThat(loggingEvents).extracting("level", "renderedMessage").containsExactly(
                tuple(Level.INFO, FIRST_MESSAGE)
                tuple(Level.INFO, SECOND_MESSAGE)
        );
    }
}

這就是為什么我們對不同消息數量的測試具有很好的靈活性

您不需要在類實現中依賴硬編碼的靜態全局 Loggers,您可以在默認構造函數中提供默認記錄器,然后使用特定構造函數設置對提供的記錄器的引用。

class MyClassToTest {
    private final Logger logger;
    
    public MyClassToTest() {
      this(SomeStatic.logger);
    };
    
    MyClassToTest(Logger logger) {
      this.logger = logger;
    };
    
    public void someOperation() {
        logger.warn("warning message");
        // ...
    };
};

class MyClassToTestTest {
    
    @Test
    public warnCalled() {
        Logger loggerMock = mock(Logger.class);
        MyClassTest myClassToTest = new MyClassToTest(logger);
        myClassToTest.someOperation();
        verify(loggerMock).warn(anyString());
    };
}

檢查這個庫https://github.com/Hakky54/log-captor

在您的 Maven 文件中包含該庫的參考:

<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>logcaptor</artifactId>
    <version>2.5.0</version>
    <scope>test</scope>
</dependency>

在 java 代碼測試方法中,您應該包括以下內容:

LogCaptor logCaptor = LogCaptor.forClass(MyClass.class);

 // do the test logic....

assertThat(logCaptor.getLogs()).contains("Some log to assert");

最簡單的方法

  @ExtendWith(OutputCaptureExtension.class)
  class MyTestClass { 
    
          @Test
          void my_test_method(CapturedOutput output) {
               assertThat(output).contains("my test log.");
          }
  }

如果您使用的是java.util.logging.Logger ,這篇文章可能會很有幫助,它會創建一個新的處理程序並在日志輸出上做出斷言:http: //octodecillion.com/blog/jmockit-test-logging/

Log4J2 的 API 略有不同。 此外,您可能正在使用它的異步附加程序。 我為此創建了一個鎖定的附加程序:

    public static class LatchedAppender extends AbstractAppender implements AutoCloseable {

    private final List<LogEvent> messages = new ArrayList<>();
    private final CountDownLatch latch;
    private final LoggerConfig loggerConfig;

    public LatchedAppender(Class<?> classThatLogs, int expectedMessages) {
        this(classThatLogs, null, null, expectedMessages);
    }
    public LatchedAppender(Class<?> classThatLogs, Filter filter, Layout<? extends Serializable> layout, int expectedMessages) {
        super(classThatLogs.getName()+"."+"LatchedAppender", filter, layout);
        latch = new CountDownLatch(expectedMessages);
        final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
        final Configuration config = ctx.getConfiguration();
        loggerConfig = config.getLoggerConfig(LogManager.getLogger(classThatLogs).getName());
        loggerConfig.addAppender(this, Level.ALL, ThresholdFilter.createFilter(Level.ALL, null, null));
        start();
    }

    @Override
    public void append(LogEvent event) {
        messages.add(event);
        latch.countDown();
    }

    public List<LogEvent> awaitMessages() throws InterruptedException {
        assertTrue(latch.await(10, TimeUnit.SECONDS));
        return messages;
    }

    @Override
    public void close() {
        stop();
        loggerConfig.removeAppender(this.getName());
    }
}

像這樣使用它:

        try (LatchedAppender appender = new LatchedAppender(ClassUnderTest.class, 1)) {

        ClassUnderTest.methodThatLogs();
        List<LogEvent> events = appender.awaitMessages();
        assertEquals(1, events.size());
        //more assertions here

    }//appender removed

另一個值得一提的想法是創建一個 CDI 生產者來注入您的記錄器,以便模擬變得容易,盡管它是一個較舊的主題。 (而且它還具有不必再聲明“整個記錄器語句”的優點,但那是題外話)

例子:

創建要注入的記錄器:

public class CdiResources {
  @Produces @LoggerType
  public Logger createLogger(final InjectionPoint ip) {
      return Logger.getLogger(ip.getMember().getDeclaringClass());
  }
}

預選賽:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface LoggerType {
}

在生產代碼中使用記錄器:

public class ProductionCode {
    @Inject
    @LoggerType
    private Logger logger;

    public void logSomething() {
        logger.info("something");
    }
}

在您的測試代碼中測試記錄器(給出一個 easyMock 示例):

@TestSubject
private ProductionCode productionCode = new ProductionCode();

@Mock
private Logger logger;

@Test
public void testTheLogger() {
   logger.info("something");
   replayAll();
   productionCode.logSomething();
}

使用 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.    
}

模擬 Appender 可以幫助捕獲日志行。 在以下位置查找示例: http ://clearqa.blogspot.co.uk/2016/12/test-log-lines.html

// Fully working test at: https://github.com/njaiswal/logLineTester/blob/master/src/test/java/com/nj/Utils/UtilsTest.java

@Test
public void testUtilsLog() throws InterruptedException {

    Logger utilsLogger = (Logger) LoggerFactory.getLogger("com.nj.utils");

    final Appender mockAppender = mock(Appender.class);
    when(mockAppender.getName()).thenReturn("MOCK");
    utilsLogger.addAppender(mockAppender);

    final List<String> capturedLogs = Collections.synchronizedList(new ArrayList<>());
    final CountDownLatch latch = new CountDownLatch(3);

    //Capture logs
    doAnswer((invocation) -> {
        LoggingEvent loggingEvent = invocation.getArgumentAt(0, LoggingEvent.class);
        capturedLogs.add(loggingEvent.getFormattedMessage());
        latch.countDown();
        return null;
    }).when(mockAppender).doAppend(any());

    //Call method which will do logging to be tested
    Application.main(null);

    //Wait 5 seconds for latch to be true. That means 3 log lines were logged
    assertThat(latch.await(5L, TimeUnit.SECONDS), is(true));

    //Now assert the captured logs
    assertThat(capturedLogs, hasItem(containsString("One")));
    assertThat(capturedLogs, hasItem(containsString("Two")));
    assertThat(capturedLogs, hasItem(containsString("Three")));
}

使用下面的代碼。 我在我的 spring 集成測試中使用相同的代碼,我使用 log back 進行日志記錄。 使用方法 assertJobIsScheduled 斷言打印在日志中的文本。

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.Appender;

private Logger rootLogger;
final Appender mockAppender = mock(Appender.class);

@Before
public void setUp() throws Exception {
    initMocks(this);
    when(mockAppender.getName()).thenReturn("MOCK");
    rootLogger = (Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
    rootLogger.addAppender(mockAppender);
}

private void assertJobIsScheduled(final String matcherText) {
    verify(mockAppender).doAppend(argThat(new ArgumentMatcher() {
        @Override
        public boolean matches(final Object argument) {
            return ((LoggingEvent)argument).getFormattedMessage().contains(matcherText);
        }
    }));
}

您可能正在嘗試測試兩件事。

  • 當我的程序的操作員感興趣的事件發生時,我的程序是否執行適當的日志記錄操作,這可以通知操作員該事件。
  • 當我的程序執行日志記錄操作時,它產生的日志消息是否具有正確的文本。

這兩個東西實際上是不同的東西,所以可以分開測試。 但是,測試第二個(消息文本)非常有問題,我建議完全不要這樣做。 消息文本的測試最終將包括檢查一個文本字符串(預期的消息文本)是否與日志記錄代碼中使用的文本字符串相同,或者可以簡單地派生自該文本字符串。

  • 這些測試根本不測試程序邏輯,它們只測試一個資源(一個字符串)是否等同於另一個資源。
  • 測試很脆弱; 即使是對日志消息格式的微小調整也會破壞您的測試。
  • 測試與日志接口的國際化(翻譯)不兼容。測試假設只有一種可能的消息文本,因此只有一種可能的人類語言。

請注意,讓您的程序代碼(可能實現一些業務邏輯)直接調用文本日志接口是糟糕的設計(但不幸的是非常常見)。 負責業務邏輯的代碼也決定了一些日志記錄策略和日志消息的文本。 它將業務邏輯與用戶界面代碼混合在一起(是的,日志消息是程序用戶界面的一部分)。 這些東西應該分開。

因此我建議業務邏輯不要直接生成日志消息的文本。 而是讓它委托給一個日志對象。

  • 日志記錄對象的類應該提供合適的內部 API,您的業務對象可以使用它來表達使用域模型對象而不是文本字符串發生的事件。
  • 您的日志記錄類的實現負責生成這些域對象的文本表示,並呈現事件的合適文本描述,然后將該文本消息轉發到低級日志記錄框架(例如 JUL、log4j 或 slf4j)。
  • 您的業​​務邏輯只負責調用 logger 類的內部 API 的正確方法,傳遞正確的域對象,以描述發生的實際事件。
  • 您的具體日志記錄類implements了一個interface ,該接口描述了您的業務邏輯可能使用的內部 API。
  • 實現業務邏輯並且必須執行日志記錄的類具有對要委托給的日志記錄對象的引用。 引用的類是抽象interface
  • 使用依賴注入來設置對記錄器的引用。

然后,您可以通過創建一個實現內部日志 API 的模擬記錄器並在測試的設置階段使用依賴注入來測試您的業務邏輯類是否正確地告訴日志接口有關事件。

像這樣:

 public class MyService {// The class we want to test
    private final MyLogger logger;

    public MyService(MyLogger logger) {
       this.logger = Objects.requireNonNull(logger);
    }

    public void performTwiddleOperation(Foo foo) {// The method we want to test
       ...// The business logic
       logger.performedTwiddleOperation(foo);
    }
 };

 public interface MyLogger {
    public void performedTwiddleOperation(Foo foo);
    ...
 };

 public final class MySl4jLogger: implements MyLogger {
    ...

    @Override
    public void performedTwiddleOperation(Foo foo) {
       logger.info("twiddled foo " + foo.getId());
    }
 }

 public final void MyProgram {
    public static void main(String[] argv) {
       ...
       MyLogger logger = new MySl4jLogger(...);
       MyService service = new MyService(logger);
       startService(service);// or whatever you must do
       ...
    }
 }

 public class MyServiceTest {
    ...

    static final class MyMockLogger: implements MyLogger {
       private Food.id id;
       private int nCallsPerformedTwiddleOperation;
       ...

       @Override
       public void performedTwiddleOperation(Foo foo) {
          id = foo.id;
          ++nCallsPerformedTwiddleOperation;
       }

       void assertCalledPerformedTwiddleOperation(Foo.id id) {
          assertEquals("Called performedTwiddleOperation", 1, nCallsPerformedTwiddleOperation);
          assertEquals("Called performedTwiddleOperation with correct ID", id, this.id);
       }
    };

    @Test
    public void testPerformTwiddleOperation_1() {
       // Setup
       MyMockLogger logger = new MyMockLogger();
       MyService service = new MyService(logger);
       Foo.Id id = new Foo.Id(...);
       Foo foo = new Foo(id, 1);

       // Execute
       service.performedTwiddleOperation(foo);

       // Verify
       ...
       logger.assertCalledPerformedTwiddleOperation(id);
    }
 }

如果我想做的只是看到記錄了一些字符串(而不是驗證太脆弱的確切日志語句),我所做的就是將 StdOut 重定向到緩沖區,執行包含,然后重置 StdOut:

PrintStream original = System.out;
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
System.setOut(new PrintStream(buffer));

// Do something that logs

assertTrue(buffer.toString().contains(myMessage));
System.setOut(original);

我為 log4j 回答了一個類似的問題,請參閱how-can-i-test-with-junit-that-a-warning-was-logged-with-log4

這是較新的示例,使用 Log4j2(使用 2.11.2 測試)和 junit 5;

    package com.whatever.log;

    import org.apache.logging.log4j.Level;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.core.Logger;
    import org.apache.logging.log4j.core.*;
    import org.apache.logging.log4j.core.appender.AbstractAppender;
    import org.apache.logging.log4j.core.config.Configuration;
    import org.apache.logging.log4j.core.config.LoggerConfig;
    import org.apache.logging.log4j.core.config.plugins.Plugin;
    import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
    import org.apache.logging.log4j.core.config.plugins.PluginElement;
    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;

    import java.util.ArrayList;
    import java.util.List;
    import static org.junit.Assert.*;

class TestLogger {

    private TestAppender testAppender;
    private LoggerConfig loggerConfig;
    private final Logger logger = (Logger)
            LogManager.getLogger(ClassUnderTest.class);

    @Test
    @DisplayName("Test Log Junit5 and log4j2")
    void test() {
        ClassUnderTest.logMessage();
        final LogEvent loggingEvent = testAppender.events.get(0);
        //asset equals 1 because log level is info, change it to debug and
        //the test will fail
        assertTrue(testAppender.events.size()==1,"Unexpected empty log");
        assertEquals(Level.INFO,loggingEvent.getLevel(),"Unexpected log level");
        assertEquals(loggingEvent.getMessage().toString()
                ,"Hello Test","Unexpected log message");
    }

    @BeforeEach
    private void setup() {
        testAppender = new TestAppender("TestAppender", null);

        final LoggerContext context = logger.getContext();
        final Configuration configuration = context.getConfiguration();

        loggerConfig = configuration.getLoggerConfig(logger.getName());
        loggerConfig.setLevel(Level.INFO);
        loggerConfig.addAppender(testAppender,Level.INFO,null);
        testAppender.start();
        context.updateLoggers();
    }

    @AfterEach
    void after(){
        testAppender.stop();
        loggerConfig.removeAppender("TestAppender");
        final LoggerContext context = logger.getContext();
        context.updateLoggers();
    }

    @Plugin( name = "TestAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE)
    static class TestAppender extends AbstractAppender {

        List<LogEvent> events = new ArrayList();

        protected TestAppender(String name, Filter filter) {
            super(name, filter, null);
        }

        @PluginFactory
        public static TestAppender createAppender(
                @PluginAttribute("name") String name,
                @PluginElement("Filter") Filter filter) {
            return new TestAppender(name, filter);
        }

        @Override
        public void append(LogEvent event) {
            events.add(event);
        }
    }

    static class ClassUnderTest {
        private static final Logger LOGGER =  (Logger) LogManager.getLogger(ClassUnderTest.class);
        public static void logMessage(){
            LOGGER.info("Hello Test");
            LOGGER.debug("Hello Test");
        }
    }
}

使用以下 Maven 依賴項

 <dependency>
 <artifactId>log4j-core</artifactId>
  <packaging>jar</packaging>
  <version>2.11.2</version>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.5.0</version>
    <scope>test</scope>
</dependency>

就我而言,我解決了與以下相同的問題:

Logger root = (Logger) LoggerFactory.getLogger(CSVTasklet.class); //CSVTasklet is my target class
    final Appender mockAppender = mock(Appender.class);
    root.addAppender(mockAppender); 

verify(mockAppender).doAppend(argThat((ArgumentMatcher) argument -> ((LoggingEvent) argument).getMessage().contains("No projects."))); // I checked "No projects." in the log

通過添加 Appender 進行單元測試並不能真正測試 Logger 的配置。 所以,我認為這是單元測試沒有帶來太多價值的獨特案例之一,但集成測試帶來了很多價值(特別是如果您的日志記錄有一些審計目的)

為了為它創建一個集成測試,讓我們假設您正在使用一個簡單的ConsoleAppender運行並且想要測試它的輸出。 然后,您應該測試消息是如何從System.out寫入其自己的ByteArrayOutputStream的。

從這個意義上說,我會執行以下操作(我使用的是 JUnit 5):

public class Slf4jAuditLoggerTest {

    private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();

    @BeforeEach
    public void beforeEach() {
        System.setOut(new PrintStream(outContent));
    }

這樣,您可以通過以下方式簡單地測試其輸出:

    @Test
    public void myTest() {
        // Given...
        // When...
        // Then
        assertTrue(outContent.toString().contains("[INFO] My formatted string from Logger"));
    }

如果您這樣做,您將為您的項目帶來更多價值,並且不需要使用內存中的實現、創建新的 Appender 或其他任何方式。

如果您使用的是 log4j2,來自https://www.dontpanicblog.co.uk/2018/04/29/test-log4j2-with-junit/的解決方案允許我斷言消息已記錄。

解決方案是這樣的:

  • 將 log4j appender 定義為 ExternalResource 規則

    public class LogAppenderResource extends ExternalResource { private static final String APPENDER_NAME = "log4jRuleAppender"; /** * Logged messages contains level and message only. * This allows us to test that level and message are set. */ private static final String PATTERN = "%-5level %msg"; private Logger logger; private Appender appender; private final CharArrayWriter outContent = new CharArrayWriter(); public LogAppenderResource(org.apache.logging.log4j.Logger logger) { this.logger = (org.apache.logging.log4j.core.Logger)logger; } @Override protected void before() { StringLayout layout = PatternLayout.newBuilder().withPattern(PATTERN).build(); appender = WriterAppender.newBuilder() .setTarget(outContent) .setLayout(layout) .setName(APPENDER_NAME).build(); appender.start(); logger.addAppender(appender); } @Override protected void after() { logger.removeAppender(appender); } public String getOutput() { return outContent.toString(); } }
  • 定義使用 ExternalResource 規則的測試

    public class LoggingTextListenerTest { @Rule public LogAppenderResource appender = new LogAppenderResource(LogManager.getLogger(LoggingTextListener.class)); private LoggingTextListener listener = new LoggingTextListener(); // Class under test @Test public void startedEvent_isLogged() { listener.started(); assertThat(appender.getOutput(), containsString("started")); } }

不要忘記將 log4j2.xml 作為 src/test/resources 的一部分

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM