Bagaimana cara JUnit menegaskan pesan di logger


206

Saya memiliki beberapa kode-dalam-tes yang memanggil Java logger untuk melaporkan statusnya. Dalam kode uji JUnit, saya ingin memverifikasi bahwa entri log yang benar dibuat di logger ini. Sesuatu di sepanjang baris berikut:

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

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

Saya kira ini bisa dilakukan dengan logger yang diadaptasi secara khusus (atau handler, atau formatter), tetapi saya lebih suka menggunakan kembali solusi yang sudah ada. (Dan, jujur ​​saja, tidak jelas bagi saya cara mendapatkan di logRecord dari logger, tetapi anggap itu mungkin.)

Jawaban:


142

Saya membutuhkan ini beberapa kali juga. Saya telah mengumpulkan sampel kecil di bawah ini, yang ingin Anda sesuaikan dengan kebutuhan Anda. Pada dasarnya, Anda membuat sendiri Appenderdan menambahkannya ke logger yang Anda inginkan. Jika Anda ingin mengumpulkan semuanya, root logger adalah tempat yang baik untuk memulai, tetapi Anda bisa menggunakan yang lebih spesifik jika Anda mau. Jangan lupa untuk menghapus Appender setelah selesai, jika tidak, Anda dapat membuat kebocoran memori. Di bawah ini saya sudah melakukannya dalam ujian, tetapi setUpatau @Beforedan tearDownatau @Aftermungkin tempat yang lebih baik, tergantung pada kebutuhan Anda.

Juga, implementasi di bawah ini mengumpulkan semua yang ada di Listdalam memori. Jika Anda banyak log, Anda mungkin mempertimbangkan untuk menambahkan filter untuk menghapus entri yang membosankan, atau untuk menulis log ke file sementara pada disk (Petunjuk: LoggingEventadalah Serializable, jadi Anda harus bisa membuat serialisasi objek acara, jika pesan log Anda adalah.)

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
Ini sangat bagus. Satu-satunya peningkatan yang akan saya lakukan adalah menelepon logger.getAllAppenders(), lalu melangkah maju dan memanggil appender.setThreshold(Level.OFF)masing-masing (dan mengatur ulang mereka ketika Anda selesai!). Ini memastikan bahwa pesan "buruk" yang Anda coba hasilkan tidak muncul di log pengujian dan membuat takut pengembang berikutnya.
Coderer

1
Dalam Log4j 2.x sedikit lebih berbelit-belit karena Anda perlu membuat plugin, lihat ini: stackoverflow.com/questions/24205093/…
paranza

1
Terima kasih untuk ini. Tetapi jika Anda menggunakan LogBack, Anda dapat menggunakan ListAppender<ILoggingEvent>alih-alih membuat append kustom Anda sendiri.
sinujohn

2
tetapi ini tidak bekerja untuk slf4j! apakah Anda tahu bagaimana saya bisa mengubahnya agar bekerja dengan itu juga?
Shilan

3
@ sd Jika Anda melemparkan Loggerke org.apache.logging.log4j.core.Logger(kelas implementasi untuk antarmuka) Anda akan mendapatkan akses setAppender()/removeAppender()lagi.
David Moles

59

Berikut ini adalah solusi Logback sederhana dan efisien.
Tidak perlu menambah / membuat kelas baru.
Itu bergantungListAppender : appender logback whitebox di mana entri log ditambahkan dalam public Listbidang yang bisa kita gunakan untuk membuat pernyataan kami.

Ini adalah contoh sederhana.

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

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

Pernyataan JUnit tidak terdengar sangat disesuaikan untuk menegaskan beberapa properti spesifik elemen daftar.
Pustaka pencocokan / pernyataan sebagai AssertJ atau Hamcrest tampak lebih baik untuk itu:

Dengan AssertJ itu akan menjadi:

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

Bagaimana Anda menghentikan tes gagal jika Anda mencatat kesalahan?
Ghilteras

@ Ghilteras saya tidak yakin untuk mengerti. Mencatat kesalahan seharusnya tidak membuat pengujian Anda gagal. Apa yang kau jelaskan
davidxxx

Juga, ingatlah untuk tidak mockkelas yang sedang diuji. Anda harus instantiate dengan newoperator
Dmytro Chasovskyi

35

Terima kasih banyak atas jawaban yang cepat dan bermanfaat ini; mereka menempatkan saya di jalan yang benar untuk solusi saya.

Basis kode yang saya ingin gunakan ini, menggunakan java.util.logging sebagai mekanisme logger, dan saya tidak merasa cukup di rumah dalam kode-kode untuk benar-benar mengubahnya ke log4j atau untuk logger antarmuka / fasad. Tapi berdasarkan saran ini, saya 'meretas' ekstensi julhandler dan itu berfungsi sebagai hadiah.

Ringkasan singkat berikut. Perpanjang 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(){}
}

Jelas, Anda dapat menyimpan sebanyak yang Anda suka / inginkan / butuhkan dari LogRecord, atau mendorong semuanya ke dalam tumpukan sampai Anda mendapatkan kelebihan.

Dalam persiapan untuk uji junit, Anda membuat java.util.logging.Loggerdan menambahkan yang baru LogHandlerke dalamnya:

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

Panggilan untuk setUseParentHandlers()adalah untuk membungkam para penangan normal, sehingga (untuk uji coba uji coba ini) tidak terjadi penebangan yang tidak perlu. Lakukan apa pun yang perlu diuji-kode oleh Anda untuk menggunakan logger ini, jalankan tes dan tegaskan Kesetaraan:

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

(Tentu saja, Anda akan memindahkan sebagian besar pekerjaan ini ke dalam @Beforemetode dan membuat berbagai macam perbaikan lainnya, tetapi itu akan mengacaukan presentasi ini.)


16

Pilihan lain adalah dengan mengejek Appender dan memverifikasi apakah pesan telah masuk ke appender ini. Contoh untuk Log4j 1.2.x dan 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

Secara efektif Anda menguji efek samping dari kelas dependen. Untuk pengujian unit, Anda hanya perlu memverifikasi itu

logger.info()

dipanggil dengan parameter yang benar. Karenanya gunakan kerangka kerja mengejek untuk meniru logger dan itu akan memungkinkan Anda untuk menguji perilaku kelas Anda sendiri.


3
Bagaimana Anda mengejek bidang final statis pribadi, yang sebagian besar penebang didefinisikan? Powermockito? Selamat bersenang-senang ..
Stefano L

Stefano: Bidang terakhir itu diinisialisasi entah bagaimana, saya telah melihat berbagai pendekatan untuk menyuntikkan Mocks daripada yang asli. Mungkin mengharuskan beberapa tingkat desain untuk testabilitas di tempat pertama. blog.codecentric.de/en/2011/11/…
djna

Seperti yang dikatakan Mehdi, mungkin menggunakan Handler yang cocok mungkin sudah cukup,
djna

11

Mengejek adalah sebuah pilihan di sini, walaupun itu akan sulit, karena para penebang pada umumnya adalah final statis pribadi - jadi pengaturan tiruan logger tidak akan menjadi sepotong kue, atau akan memerlukan modifikasi kelas yang sedang diuji.

Anda dapat membuat Appender khusus (atau apa pun namanya), dan mendaftarkannya - baik melalui file konfigurasi hanya tes, atau runtime (dengan cara, tergantung pada kerangka kerja logging). Dan kemudian Anda bisa mendapatkan appender itu (baik secara statis, jika dideklarasikan dalam file konfigurasi, atau dengan referensi saat ini, jika Anda memasukkannya runtime), dan memverifikasi isinya.


10

Terinspirasi oleh solusi @ RonaldBlaschke, saya datang dengan ini:

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

}

... yang memungkinkan Anda melakukan:

@Rule public Log4JTester logTest = new Log4JTester();

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

Anda mungkin bisa membuatnya menggunakan hamcrest dengan cara yang lebih cerdas, tapi saya sudah membiarkannya.


6

Untuk log4j2 solusinya sedikit berbeda karena AppenderSkeleton tidak tersedia lagi. Selain itu, menggunakan Mockito, atau pustaka sejenis untuk membuat Appender dengan ArgumentCaptor tidak akan berfungsi jika Anda mengharapkan beberapa pesan logging karena MutableLogEvent digunakan kembali pada beberapa pesan log. Solusi terbaik yang saya temukan untuk log4j2 adalah:

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

Seperti disebutkan dari yang lain, Anda bisa menggunakan kerangka mengejek. Agar ini berfungsi, Anda harus mengekspos logger di kelas Anda (walaupun saya lebih suka membuatnya menjadi paket pribadi daripada membuat setter publik).

Solusi lainnya adalah membuat logger palsu dengan tangan. Anda harus menulis logger palsu (kode fixture lebih banyak) tetapi dalam kasus ini saya lebih suka peningkatan keterbacaan tes terhadap kode yang disimpan dari kerangka kerja mengejek.

Saya akan melakukan sesuatu seperti ini:

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

Wow. Saya tidak yakin mengapa ini sangat sulit. Saya menemukan saya tidak dapat menggunakan contoh kode di atas karena saya menggunakan log4j2 lebih dari slf4j. Ini solusi saya:

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

Inilah yang saya lakukan untuk logback.

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

Kemudian di induk dari kelas tes unit tes saya, saya membuat metode:

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

Saya memiliki file logback-test.xml yang didefinisikan dalam src / test / resources dan saya menambahkan appender test:

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

dan menambahkan append ini ke root append:

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

Sekarang di kelas tes saya yang diperluas dari kelas tes orang tua saya, saya bisa mendapatkan appender dan mendapatkan pesan terakhir yang dicatat dan memverifikasi pesan, tingkat, yang bisa dibuang.

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

Saya tidak melihat di mana metode getAppender didefinisikan?!?
bioinfornatika

getAppender adalah metode di ch.qos.logback.classic.Logger
kfox

4

Untuk Junit 5 (Jupiter) Spring's OutputCaptureExtension cukup berguna. Ini tersedia sejak Spring Boot 2.2 dan tersedia di artefak uji pegas boot .

Contoh (diambil dari 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");
    }
}

Saya percaya pernyataan log tidak seperti getOut()atau getErr().
Ram

Ini adalah jawaban yang saya cari (walaupun pertanyaannya bukan terkait boot spring)!
helleye

3

Sedangkan bagi saya, Anda dapat menyederhanakan tes Anda dengan menggunakan JUnitdengan Mockito. Saya mengusulkan solusi berikut untuk itu:

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

Itu sebabnya kami memiliki fleksibilitas yang bagus untuk pengujian dengan jumlah pesan yang berbeda


1
Untuk tidak mengulangi blok kode yang hampir sama ingin menambahkan bahwa hampir 1to1 berfungsi untuk saya untuk Log4j2. Hanya mengubah impor ke "org.apache.logging.log4j.core", cor logger ke "org.apache.logging.log4j.core.Logger", tambahkan when(appender.isStarted()).thenReturn(true); when(appender.getName()).thenReturn("Test Appender"); dan ubah 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
Ini berhasil untuk saya. Baris 'when (mockAppender.getName ()). ThenReturn ("MOCK")' tidak diperlukan untuk saya.
Mayank Raghav

1

API untuk Log4J2 sedikit berbeda. Anda juga mungkin menggunakan aplikasi async. Saya membuat append latch untuk ini:

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

Gunakan seperti ini:

        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

Perhatikan bahwa di Log4J 2.x, antarmuka publik org.apache.logging.log4j.Loggertidak termasuk setAppender()dan removeAppender()metode.

Tetapi jika Anda tidak melakukan sesuatu yang terlalu mewah, Anda harus dapat melemparkannya ke kelas implementasi org.apache.logging.log4j.core.Logger, yang mengekspos metode tersebut.

Berikut ini contoh dengan Mockito dan AssertJ :

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

Gagasan lain yang layak disebut, meskipun ini adalah topik yang lebih lama, adalah menciptakan produser CDI untuk menyuntikkan logger Anda sehingga mengejek menjadi mudah. (Dan itu juga memberi keuntungan karena tidak harus mendeklarasikan "pernyataan logger keseluruhan" lagi, tapi itu di luar topik)

Contoh:

Membuat logger untuk disuntikkan:

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

Kualifikasi:

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

Menggunakan logger dalam kode produksi Anda:

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

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

Menguji logger dalam kode pengujian Anda (memberikan contoh easyMock):

@TestSubject
private ProductionCode productionCode = new ProductionCode();

@Mock
private Logger logger;

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

0

Menggunakan Jmockit (1.21) saya bisa menulis tes sederhana ini. Tes ini memastikan pesan ERROR tertentu dipanggil hanya sekali.

@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

Mengejek Appender dapat membantu menangkap garis log. Temukan sampel di: 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

Gunakan kode di bawah ini. Saya menggunakan kode yang sama untuk tes integrasi pegas saya di mana saya menggunakan log kembali untuk logging. Gunakan metode assertJobIsScheduled untuk menegaskan teks yang dicetak dalam log.

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

Ada dua hal yang mungkin ingin Anda coba.

  • Ketika ada acara yang menarik bagi operator program saya, apakah program saya melakukan operasi logging yang sesuai, yang dapat memberi tahu operator tentang peristiwa itu.
  • Ketika program saya melakukan operasi logging, apakah pesan log yang dihasilkannya memiliki teks yang benar.

Kedua hal tersebut sebenarnya adalah hal yang berbeda, sehingga dapat diuji secara terpisah. Namun, pengujian yang kedua (teks pesan) sangat bermasalah, saya sarankan untuk tidak melakukannya sama sekali. Pengujian teks pesan pada akhirnya akan terdiri dari pengecekan bahwa satu string teks (teks pesan yang diharapkan) sama dengan, atau dapat diperoleh dari, string teks yang digunakan dalam kode logging Anda.

  • Tes-tes tersebut tidak menguji logika program sama sekali, mereka hanya menguji bahwa satu sumber daya (string) setara dengan sumber daya lain.
  • Tes rapuh; bahkan sedikit perubahan pada pemformatan pesan log akan merusak pengujian Anda.
  • Tes tidak kompatibel dengan internasionalisasi (terjemahan) dari antarmuka logging Anda. Tes menganggap hanya ada satu teks pesan yang mungkin, dan dengan demikian hanya satu bahasa manusia yang mungkin.

Perhatikan bahwa memiliki kode program Anda (menerapkan logika bisnis, mungkin) secara langsung memanggil antarmuka pembuatan log teks adalah desain yang buruk (tapi sayangnya sangat umum). Kode yang bertanggung jawab untuk logika bisnis juga memutuskan beberapa kebijakan pencatatan dan teks pesan log. Ini mencampur logika bisnis dengan kode antarmuka pengguna (ya, pesan log adalah bagian dari antarmuka pengguna program Anda). Hal-hal itu harus terpisah.

Karena itu saya merekomendasikan bahwa logika bisnis tidak secara langsung menghasilkan teks pesan log. Alih-alih minta didelegasikan ke objek logging.

  • Kelas objek logging harus menyediakan API internal yang cocok, yang objek bisnis Anda dapat gunakan untuk mengekspresikan peristiwa yang telah terjadi menggunakan objek model domain Anda, bukan string teks.
  • Implementasi kelas logging Anda bertanggung jawab untuk menghasilkan representasi teks dari objek domain tersebut, dan memberikan deskripsi teks yang sesuai dari acara tersebut, kemudian meneruskan pesan teks tersebut ke kerangka logging level rendah (seperti JUL, log4j atau slf4j).
  • Logika bisnis Anda hanya bertanggung jawab untuk memanggil metode yang benar dari API internal kelas logger Anda, melewati objek domain yang benar, untuk menggambarkan peristiwa aktual yang terjadi.
  • Kelas Anda penebangan beton implementssebuahinterface , yang menggambarkan API internal yang logika bisnis Anda dapat menggunakan.
  • Kelas Anda yang menerapkan logika bisnis dan harus melakukan pencatatan memiliki referensi ke objek pencatatan untuk didelegasikan. Kelas referensi adalah abstrak interface.
  • Gunakan injeksi ketergantungan untuk mengatur referensi ke logger.

Anda kemudian dapat menguji bahwa kelas logika bisnis Anda dengan benar memberi tahu antarmuka pencatatan tentang peristiwa, dengan membuat mock logger, yang mengimplementasikan API pencatatan internal, dan menggunakan injeksi ketergantungan pada fase pengaturan pengujian Anda.

Seperti ini:

 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

Apa yang telah saya lakukan jika semua yang ingin saya lakukan adalah melihat bahwa beberapa string telah dicatat (tidak seperti memverifikasi pernyataan log yang terlalu rapuh) adalah untuk mengarahkan StdOut ke buffer, melakukan isi, lalu reset 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
Saya mencoba ini dengan java.util.logging(meskipun saya gunakan System.setErr(new PrintStream(buffer));, karena log ke stderr), tetapi tidak berfungsi (buffer tetap kosong). jika saya menggunakan System.err.println("foo")secara langsung, ia berfungsi, jadi saya berasumsi bahwa sistem logging menyimpan referensi sendiri dari aliran output, yang diambil darinya System.err, jadi panggilan saya untuk System.setErr(..)tidak berpengaruh pada output log, seperti yang terjadi setelah sistem log init.
hoijui

0

Saya menjawab pertanyaan serupa untuk log4j lihat bagaimana-bisa-saya-uji-dengan-junit-bahwa-peringatan-adalah-login-dengan-log4

Ini lebih baru dan contoh dengan Log4j2 (diuji dengan 2.11.2) dan 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");
        }
    }
}

Menggunakan dependensi pakar berikut

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

Saya mencoba ini dan mendapat kesalahan di dalam metode pengaturan pada baris loggerConfig = configuration.getLoggerConfig (logger.getName ()); Kesalahan tidak dapat mengakses file kelas org.apache.logging.log4j.spiShutdownEnabled untuk org.apache.logging.log4j.spi.LoggerContextShutdownEnabled tidak ditemukan
carlos palma

Saya meninjau kode dan membuat beberapa perubahan kecil, tetapi berhasil bagi saya. Saya sarankan Anda akan memeriksa dependensi dan memastikan bahwa semua impor sudah benar
Haim Raman

Halo, Haim. Saya akhirnya mengimplementasikan solusi logback ... tapi saya pikir Anda benar, untuk mengimplementasikan yang saya harus membersihkan impor yang saya buat dari versi log4j lain.
carlos palma

-1

Jika Anda menggunakan log4j2, solusi dari https://www.dontpanicblog.co.uk/2018/04/29/test-log4j2-with-junit/ memungkinkan saya untuk menegaskan bahwa pesan telah dicatat.

Solusinya seperti ini:

  • Tetapkan app4 log app sebagai aturan 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();
        }
    }
  • Tetapkan tes yang menggunakan aturan ExternalResource Anda

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

Jangan lupa untuk memiliki log4j2.xml sebagai bagian dari src / test / resources

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.