Cách thực hiện JUnit khẳng định thông điệp trong logger


206

Tôi có một số bài kiểm tra mã yêu cầu trình ghi nhật ký Java để báo cáo trạng thái của nó. Trong mã kiểm tra JUnit, tôi muốn xác minh rằng mục nhập nhật ký chính xác đã được thực hiện trong logger này. Một cái gì đó dọc theo các dòng sau:

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

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

Tôi cho rằng điều này có thể được thực hiện với một trình ghi nhật ký thích nghi đặc biệt (hoặc trình xử lý hoặc trình định dạng), nhưng tôi thích sử dụng lại một giải pháp đã tồn tại. (Và, thành thật mà nói, tôi không rõ làm thế nào để truy cập logRecord từ một logger, nhưng giả sử rằng điều đó là có thể.)

Câu trả lời:


142

Tôi cũng cần điều này nhiều lần. Tôi đã tập hợp một mẫu nhỏ bên dưới, bạn muốn điều chỉnh theo nhu cầu của mình. Về cơ bản, bạn tạo riêng của bạn Appendervà thêm nó vào logger bạn muốn. Nếu bạn muốn thu thập mọi thứ, logger gốc là một nơi tốt để bắt đầu, nhưng bạn có thể sử dụng một cách cụ thể hơn nếu bạn muốn. Đừng quên xóa Appender khi bạn hoàn thành, nếu không bạn có thể tạo rò rỉ bộ nhớ. Dưới đây tôi đã thực hiện nó trong kiểm tra, nhưng setUphay @BeforetearDownhay @Aftercó thể là nơi tốt hơn, tùy thuộc vào nhu cầu của bạn.

Ngoài ra, việc thực hiện dưới đây thu thập mọi thứ Listtrong bộ nhớ. Nếu bạn đang đăng nhập rất nhiều bạn có thể cân nhắc việc thêm một bộ lọc để thả nhàm chán mục, hoặc để viết nhật ký vào một tập tin tạm thời trên đĩa (Gợi ý: LoggingEventSerializable, vì vậy bạn nên có thể chỉ cần serialize các đối tượng sự kiện, nếu nhắn tin đăng nhập của bạn Là.)

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

4
Điều này làm việc tuyệt vời. Cải tiến duy nhất tôi sẽ thực hiện là gọi logger.getAllAppenders(), sau đó bước qua và gọi appender.setThreshold(Level.OFF)từng cái (và đặt lại chúng khi bạn hoàn tất!). Điều này đảm bảo rằng các thông báo "xấu" mà bạn đang cố gắng tạo không hiển thị trong nhật ký kiểm tra và khiến nhà phát triển tiếp theo hoảng sợ.
Bộ giải mã

1
Trong Log4j 2.x phức tạp hơn một chút khi bạn cần tạo một plugin, hãy xem cái này: stackoverflow.com/questions/24205093/
Lỗi

1
Cảm ơn vì điều đó. Nhưng nếu bạn đang sử dụng LogBack, bạn có thể sử dụng ListAppender<ILoggingEvent>thay vì tạo ứng dụng tùy chỉnh của riêng bạn.
sinujohn

2
nhưng điều này không làm việc cho slf4j! Bạn có biết làm thế nào tôi có thể thay đổi nó để làm việc với điều đó không?
Shilan

3
@sd Nếu bạn Loggerchuyển sang org.apache.logging.log4j.core.Logger(lớp triển khai cho giao diện), bạn sẽ có quyền truy cập setAppender()/removeAppender()lại.
David Moles

59

Đây là một giải pháp Logback đơn giản và hiệu quả.
Nó không yêu cầu thêm / tạo bất kỳ lớp mới.
Nó dựa vào ListAppender: một appender logback whitebox nơi các mục nhật ký được thêm vào trong một public Listtrường mà chúng ta có thể sử dụng để thực hiện các xác nhận của mình.

Đây là một ví dụ đơn giản.

Lớp 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");
    }
}

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

Các xác nhận của JUnit không có vẻ rất thích nghi để khẳng định một số thuộc tính cụ thể của các thành phần danh sách.
Các thư viện đối sánh / xác nhận là AssertJ hoặc Hamcrest xuất hiện tốt hơn cho điều đó:

Với AssertJ, nó sẽ là:

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

Làm thế nào để bạn dừng kiểm tra không thành công nếu bạn đăng nhập Lỗi?
Ghilteras

@Ghilteras Tôi không chắc chắn để hiểu. Ghi nhật ký một lỗi sẽ không làm cho bài kiểm tra của bạn thất bại. Bạn giải thích gì?
davidxxx

Ngoài ra, hãy nhớ không phải mocklớp đang được kiểm tra. Bạn cần khởi tạo nó với newnhà điều hành
Dmytro Chasovskyi

35

Cảm ơn rất nhiều cho những câu trả lời nhanh chóng và hữu ích này (đáng ngạc nhiên); họ đưa tôi đi đúng hướng cho giải pháp của tôi.

Cơ sở mã mà tôi muốn sử dụng, sử dụng java.util.logging làm cơ chế logger của nó và tôi không cảm thấy ở nhà đủ các mã đó để thay đổi hoàn toàn thành log4j hoặc giao diện / mặt tiền của logger. Nhưng dựa trên những gợi ý này, tôi đã "hack" một phần mở rộng của julhandler và nó hoạt động như một điều trị.

Một bản tóm tắt ngắn gọn sau đây. Mở rộng 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(){}
}

Rõ ràng, bạn có thể lưu trữ bao nhiêu tùy thích / muốn / cần từ LogRecordhoặc đẩy tất cả chúng vào một ngăn xếp cho đến khi bạn nhận được một tràn.

Trong quá trình chuẩn bị cho bài kiểm tra Junit, bạn tạo một java.util.logging.Loggervà thêm một cái mới như vậy LogHandlervào nó:

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

Lời kêu gọi setUseParentHandlers()là để bịt miệng những người xử lý bình thường, để (đối với lần chạy thử nghiệm này) không có đăng nhập không cần thiết xảy ra. Làm bất cứ điều gì mà bài kiểm tra mã của bạn cần để sử dụng bộ ghi này, chạy thử nghiệm và khẳng định:

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

(Tất nhiên, bạn sẽ chuyển phần lớn công việc này thành một @Beforephương thức và thực hiện các loại cải tiến khác, nhưng điều đó sẽ làm lộn xộn bài thuyết trình này.)


16

Một tùy chọn khác là giả định Appender và xác minh xem tin nhắn đã được đăng nhập vào appender này chưa. Ví dụ cho Log4j 1.2.x và 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);
    }
}

16

Thực tế, bạn đang thử nghiệm tác dụng phụ của lớp phụ thuộc. Đối với thử nghiệm đơn vị, bạn chỉ cần xác minh rằng

logger.info()

được gọi với tham số chính xác. Do đó, sử dụng khung mô phỏng để mô phỏng logger và điều đó sẽ cho phép bạn kiểm tra hành vi của lớp mình.


3
Làm thế nào bạn giả định một trường cuối cùng tĩnh riêng tư, mà hầu hết các logger được xác định? Powermockito? Hãy vui vẻ ..
Stefano L

Stefano: Lĩnh vực cuối cùng đó đã được khởi tạo bằng cách nào đó, tôi đã thấy nhiều cách tiếp cận khác nhau để tiêm Mocks hơn là thực tế. Có lẽ yêu cầu một số mức độ thiết kế để kiểm tra ở nơi đầu tiên. blog.codecentric.de/en/2011/11/ từ
djna

Như Mehdi đã nói, có thể sử dụng một Handler phù hợp có thể là đủ,
djna

11

Mocking là một tùy chọn ở đây, mặc dù nó sẽ khó, bởi vì các logger thường là cuối cùng tĩnh riêng - vì vậy, thiết lập một logger giả sẽ không phải là một miếng bánh, hoặc sẽ yêu cầu sửa đổi lớp đang thử nghiệm.

Bạn có thể tạo Appender tùy chỉnh (hoặc bất cứ thứ gì nó được gọi) và đăng ký nó - thông qua tệp cấu hình chỉ kiểm tra hoặc thời gian chạy (theo cách nào đó, phụ thuộc vào khung đăng nhập). Và sau đó bạn có thể có được appender đó (hoặc tĩnh, nếu được khai báo trong tệp cấu hình hoặc bằng tham chiếu hiện tại của nó, nếu bạn đang cắm thời gian chạy) và xác minh nội dung của nó.


10

Lấy cảm hứng từ giải pháp của @ RonaldBlaschke, tôi đã nghĩ ra điều này:

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

}

... cho phép bạn làm:

@Rule public Log4JTester logTest = new Log4JTester();

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

Bạn có thể làm cho nó sử dụng hamc theo cách thông minh hơn, nhưng tôi đã để nó ở đây.


6

Đối với log4j2, giải pháp hơi khác một chút vì AppenderSkeleton không còn khả dụng nữa. Ngoài ra, sử dụng Mockito hoặc thư viện tương tự để tạo Appender bằng ArgumentCaptor sẽ không hoạt động nếu bạn mong đợi nhiều thông điệp đăng nhập vì MutableLogEvent được sử dụng lại qua nhiều thông điệp tường trình. Giải pháp tốt nhất tôi tìm thấy cho log4j2 là:

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

5

Như đã đề cập từ những người khác, bạn có thể sử dụng một khung mô phỏng. Để làm việc này, bạn phải hiển thị bộ ghi chép trong lớp của mình (mặc dù tôi có thể sẵn sàng để đặt gói riêng tư thay vì tạo một bộ cài đặt công khai).

Giải pháp khác là tạo một logger giả bằng tay. Bạn phải viết logger giả (mã cố định nhiều hơn) nhưng trong trường hợp này tôi thích khả năng đọc nâng cao của các bài kiểm tra đối với mã đã lưu từ khung mô phỏng.

Tôi sẽ làm một cái gì đó như thế này:

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

5

Ồ Tôi không chắc tại sao điều này lại khó khăn đến vậy. Tôi thấy rằng tôi không thể sử dụng bất kỳ mẫu mã nào ở trên vì tôi đang sử dụng log4j2 trên slf4j. Đây là giải pháp của tôi:

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

4

Đây là những gì tôi đã làm cho logback.

Tôi đã tạo một lớp 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();
    }
}

Sau đó, trong lớp cha mẹ của lớp kiểm tra đơn vị testng của tôi, tôi đã tạo ra một phương thức:

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

Tôi có một tệp logback-test.xml được xác định trong src / test / resource và tôi đã thêm một appender thử nghiệm:

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

và thêm ứng dụng này vào ứng dụng gốc:

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

Bây giờ trong các lớp kiểm tra của tôi mở rộng từ lớp kiểm tra cha mẹ của tôi, tôi có thể nhận được ứng dụng và nhận được thông báo cuối cùng được ghi lại và xác minh tin nhắn, cấp độ, có thể ném được.

ILoggingEvent lastEvent = testAppender.getLastEvent();
assertEquals(lastEvent.getMessage(), "...");
assertEquals(lastEvent.getLevel(), Level.WARN);
assertEquals(lastEvent.getThrowableProxy().getMessage(), "...");

Tôi không thấy phương thức getAppender được định nghĩa ở đâu?!?
tin sinh học

getAppender là một phương thức trên ch.qos.logback. classic.Logger
kfox

4

Đối với Junit 5 (Sao Mộc) Đầu ra của SpringCaptureExtension khá hữu ích. Nó có sẵn từ Spring Boot 2.2 và có sẵn trong bài kiểm tra khởi động mùa xuân tạo phẩm .

Ví dụ (lấy từ 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");
    }
}

Tôi tin rằng báo cáo nhật ký là không giống getOut()hoặc getErr().
Ram

Đây là câu trả lời tôi đang tìm kiếm (mặc dù câu hỏi không liên quan đến khởi động mùa xuân)!
helleye

3

Đối với tôi, bạn có thể đơn giản hóa bài kiểm tra của mình bằng cách sử dụng JUnitvới Mockito. Tôi đề xuất giải pháp sau đây cho nó:

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

Đó là lý do tại sao chúng tôi có sự linh hoạt tốt cho các bài kiểm tra với số lượng tin nhắn khác nhau


1
Để không lặp lại gần như các khối mã giống nhau muốn thêm rằng gần như 1to1 hoạt động với tôi cho Log4j2. Chỉ cần thay đổi nhập khẩu thành "org.apache.logging.log4j.core", chuyển logger thành "org.apache.logging.log4j.core.Logger", thêm when(appender.isStarted()).thenReturn(true); when(appender.getName()).thenReturn("Test Appender"); và thay đổi LoggingEvent -> LogEvent
Aliaksei Yatsau

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

1
Điều này làm việc cho tôi. Dòng 'khi (mockAppender.getName ()). ThenReturn ("MOCK")' không cần thiết cho tôi.
Mayank Raghav

1

API cho Log4J2 hơi khác một chút. Ngoài ra, bạn có thể đang sử dụng appender async của nó. Tôi đã tạo một ứng dụng chốt cho việc này:

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

Sử dụng nó như thế này:

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

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

    }//appender removed

1

Lưu ý rằng trong Log4J 2.x, giao diện chung org.apache.logging.log4j.Loggerkhông bao gồm setAppender()removeAppender() phương thức .

Nhưng nếu bạn không làm bất cứ điều gì quá hấp dẫn, bạn sẽ có thể chuyển nó đến lớp thực hiện org.apache.logging.log4j.core.Logger , điều này làm lộ ra các phương thức đó.

Đây là một ví dụ với 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);

0

Một ý tưởng khác đáng được đề cập, mặc dù đó là một chủ đề cũ hơn, là tạo ra một nhà sản xuất CDI để tiêm logger của bạn để việc chế nhạo trở nên dễ dàng. (Và nó cũng mang lại lợi thế là không phải khai báo "toàn bộ câu lệnh logger" nữa, nhưng đó là ngoài chủ đề)

Thí dụ:

Tạo logger để tiêm:

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

Vòng loại:

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

Sử dụng bộ ghi trong mã sản xuất của bạn:

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

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

Kiểm tra bộ ghi trong mã kiểm tra của bạn (đưa ra ví dụ easyMock):

@TestSubject
private ProductionCode productionCode = new ProductionCode();

@Mock
private Logger logger;

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

0

Sử dụng Jmockit (1.21) tôi đã có thể viết bài kiểm tra đơn giản này. Kiểm tra đảm bảo một thông điệp LRI cụ thể được gọi chỉ một lần.

@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.    
}

0

Mocking Appender có thể giúp nắm bắt các dòng nhật ký. Tìm mẫu trên: 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")));
}

0

Sử dụng mã dưới đây. Tôi đang sử dụng cùng một mã cho bài kiểm tra tích hợp mùa xuân của mình, nơi tôi đang sử dụng đăng nhập lại để đăng nhập. Sử dụng phương thức assertJobIsSchediated để xác nhận văn bản được in trong nhật ký.

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


0

Có hai điều mà bạn có thể đang cố gắng kiểm tra.

  • Khi có một sự kiện quan tâm đến người điều hành chương trình của tôi, chương trình của tôi có thực hiện thao tác ghi nhật ký thích hợp hay không, có thể thông báo cho người vận hành sự kiện đó.
  • Khi chương trình của tôi thực hiện thao tác ghi nhật ký, thông điệp tường trình mà nó tạo ra có văn bản chính xác không.

Hai thứ đó thực sự là những thứ khác nhau, và do đó có thể được kiểm tra riêng. Tuy nhiên, việc kiểm tra thứ hai (văn bản của tin nhắn) rất khó khăn, tôi khuyên bạn không nên làm điều đó. Một bài kiểm tra văn bản tin nhắn cuối cùng sẽ bao gồm việc kiểm tra xem một chuỗi văn bản (văn bản tin nhắn dự kiến) có giống như hoặc có thể được lấy từ một cách tầm thường từ chuỗi văn bản được sử dụng trong mã đăng nhập của bạn.

  • Các thử nghiệm đó hoàn toàn không kiểm tra logic chương trình, chúng chỉ kiểm tra rằng một tài nguyên (một chuỗi) tương đương với tài nguyên khác.
  • Các xét nghiệm rất mong manh; thậm chí một điều chỉnh nhỏ đối với định dạng của thông điệp tường trình sẽ phá vỡ các bài kiểm tra của bạn.
  • Các bài kiểm tra không tương thích với quốc tế hóa (bản dịch) giao diện ghi nhật ký của bạn. Các bài kiểm tra giả định rằng chỉ có một văn bản tin nhắn khả thi và do đó chỉ có một ngôn ngữ con người có thể.

Lưu ý rằng có mã chương trình của bạn (có thể thực hiện một số logic nghiệp vụ), có thể gọi trực tiếp giao diện ghi nhật ký văn bản là thiết kế kém (nhưng không may là rất phổ biến). Mã chịu trách nhiệm về logic nghiệp vụ cũng đang quyết định một số chính sách ghi nhật ký và văn bản của thông điệp tường trình. Nó trộn logic kinh doanh với mã giao diện người dùng (vâng, thông điệp tường trình là một phần của giao diện người dùng của chương trình của bạn). Những thứ đó nên riêng biệt.

Do đó, tôi khuyên rằng logic nghiệp vụ không trực tiếp tạo ra văn bản của thông điệp tường trình. Thay vào đó, nó ủy thác cho một đối tượng đăng nhập.

  • Lớp của đối tượng ghi nhật ký nên cung cấp API nội bộ phù hợp, mà đối tượng kinh doanh của bạn có thể sử dụng để thể hiện sự kiện đã xảy ra bằng cách sử dụng các đối tượng của mô hình miền của bạn, chứ không phải chuỗi văn bản.
  • Việc triển khai lớp ghi nhật ký của bạn chịu trách nhiệm tạo ra các biểu diễn văn bản của các đối tượng miền đó và hiển thị mô tả văn bản phù hợp của sự kiện, sau đó chuyển tiếp tin nhắn văn bản đó đến khung ghi nhật ký cấp thấp (như JUL, log4j hoặc slf4j).
  • Logic nghiệp vụ của bạn chỉ chịu trách nhiệm gọi các phương thức chính xác của API nội bộ của lớp logger của bạn, truyền các đối tượng miền chính xác, để mô tả các sự kiện thực tế đã xảy ra.
  • Bê tông khai thác gỗ của bạn lớp implementsmột interface, trong đó mô tả các API nội logic kinh doanh của bạn có thể sử dụng.
  • (Các) lớp của bạn thực hiện logic nghiệp vụ và phải thực hiện ghi nhật ký có tham chiếu đến đối tượng ghi nhật ký để ủy quyền. Các lớp của tài liệu tham khảo là trừu tượng interface.
  • Sử dụng phép nội xạ phụ thuộc để thiết lập tham chiếu đến logger.

Sau đó, bạn có thể kiểm tra rằng các lớp logic nghiệp vụ của bạn cho biết chính xác giao diện ghi nhật ký về các sự kiện, bằng cách tạo một trình ghi nhật ký giả, thực hiện API ghi nhật ký nội bộ và sử dụng phép nội xạ phụ thuộc trong giai đoạn thiết lập thử nghiệm của bạn.

Như thế này:

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

0

Những gì tôi đã làm nếu tất cả những gì tôi muốn làm là thấy rằng một số chuỗi đã được ghi lại (trái ngược với việc xác minh các báo cáo nhật ký chính xác quá dễ vỡ) là chuyển hướng StdOut sang bộ đệm, thực hiện chứa, sau đó đặt lại 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);

1
Tôi đã thử điều này với java.util.logging(mặc dù tôi đã sử dụng System.setErr(new PrintStream(buffer));, vì nó đăng nhập vào stderr), nhưng nó không hoạt động (bộ đệm vẫn trống). nếu tôi sử dụng System.err.println("foo")trực tiếp, nó hoạt động, vì vậy tôi giả sử rằng hệ thống ghi nhật ký giữ tham chiếu riêng của luồng đầu ra, do đó System.err, lệnh gọi của tôi System.setErr(..)không ảnh hưởng đến đầu ra nhật ký, vì nó xảy ra sau khi khởi động hệ thống nhật ký.
hoijui

0

Tôi đã trả lời một câu hỏi tương tự cho log4j xem how-can-i-test-with-junit-that-a-warning-was-log-with-log4

Đây là phiên bản mới hơn và ví dụ với Log4j2 (được thử nghiệm với 2.11.2) và 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");
        }
    }
}

Sử dụng các phụ thuộc maven sau

 <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>

Tôi đã thử điều này và gặp lỗi trong phương thức thiết lập trên dòng loggerConfig = configure.getLoggerConfig (logger.getName ()); Lỗi không thể truy cập org.apache.logging.log4j.spi.LoggerContextShutdownEnables tệp cho org.apache.logging.log4j.spi.LoggerContextShutdownEnatted không tìm thấy
carlos palma

Tôi đã xem lại mã và thực hiện một số thay đổi nhỏ, nhưng nó đã làm việc cho tôi. Tôi đề nghị bạn sẽ kiểm tra các phụ thuộc và đảm bảo rằng tất cả nhập khẩu là chính xác
Haim Raman

Xin chào Haim. Cuối cùng tôi đã thực hiện giải pháp logback ... nhưng tôi nghĩ rằng bạn đã đúng, để thực hiện rằng tôi phải xóa một bản nhập mà tôi đã tạo từ một phiên bản log4j khác.
carlos palma

-1

Nếu bạn đang sử dụng log4j2, giải pháp từ https://www.dontpanicblog.co.uk/2018/04/29/test-log4j2-with-junit/ cho phép tôi xác nhận tin nhắn đã được ghi lại.

Giải pháp như sau:

  • Xác định một app4 log4j như là một quy tắc InternalResource

    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();
        }
    }
  • Xác định một bài kiểm tra sử dụng quy tắc InternalResource của bạn

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

Đừng quên để log4j2.xml là một phần của src / test / resource

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.