添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Most of the time you would like to inject dependencies to your class directly – through the class constructor or by setting required properties via setters. No matter what programming style you prefer you may encounter situation when some dependency cannot or shouldn’t be passed directly to your system under test.

Good example of that kind of situation are loggers – most of the time we want to access them directly via static methods. I will show you ideas how to effectively use SLF4J with Logback in unit tests.

Let’s suppose that your code encounter very specific race condition. You fixed the bug and now you want to have information in your monitoring services if something like this happens again. Those race conditions may be difficult to reproduce in full environment so instead of clicking through app you write automatic test for that case.

How to mock SLF4J Logger?

https://www.slf4j.org/

SLF4J is default logger added to Spring Boot projects. There are high chances that you are already using it.

Let’s consider the following RestController:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@RestController
public class Controller {
    private final ItemsService service;
    private final Logger logger = LoggerFactory.getLogger(Controller.class);
    public Controller(ItemsService service) {
        this.service = service;
    @GetMapping
    public List<Entity> getAll() {
        try {
            return service.getAll();
        } catch (Exception exception) {
            logger.warn("Service.getAll returned exception instead of empty list", exception);
            return Collections.emptyList();

As you can see, in this case we log warn when service throws exception, but we are not passing that error to client, rather log it for monitoring purposes.

We could try to move logger into constructor parameter and just change it’s implementation for unit tests. That’s not always possible due to several reasons (legacy code, team programming style, etc.). We will try to provide test double for logger without system under test modification – it will stay private final.

How to assert that SLF4J logged warn?

We will:

private final Logger logger = LoggerFactory.getLogger(Controller.class);
public Controller(ItemsService service) {
    this.service = service;
    System.out.println((logger.getClass().toString())); //class ch.qos.logback.classic.Logger

For testing purpose we can cast Logger to it’s underlying implementation:

LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger
import ch.qos.logback.core.AppenderBase
class TestAppender : AppenderBase<ILoggingEvent>() {
    private val events: MutableList<ILoggingEvent> = mutableListOf()
    fun lastLoggedEvent() = events.lastOrNull()
    override fun append(eventObject: ILoggingEvent) {
        events.add(eventObject)

Logs that come to appender are added to events list

Now we can write rest of the test:

import ch.qos.logback.classic.Level
import ch.qos.logback.classic.Logger
import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
import org.slf4j.LoggerFactory
class ControllerLoggingTest : StringSpec({
    "given service error when get all called then log warn" {
        //prepare logging context
        val testAppender = TestAppender()
        val logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger
        logger.addAppender(testAppender)
        testAppender.start()
        //setup system under test
        val controller = Controller(mock {
            on { getAll() } doThrow Exception("Something failed :(")
        //execute system under test
        controller.getAll()
        //capture last logged event
        val lastLoggedEvent = testAppender.lastLoggedEvent()
        require(lastLoggedEvent != null) {
            "There are on events appended in TestLogger"
        //assertion on last logged event fields
        assertSoftly {
            lastLoggedEvent.level shouldBe Level.WARN
            lastLoggedEvent.message shouldContain "Service.getAll returned exception instead of empty list"
class TestLogAppender extends AppenderBase<ILoggingEvent> {
    ArrayList<ILoggingEvent> loggingEvents = new ArrayList<>();
    @Override
    protected void append(ILoggingEvent eventObject) {
        loggingEvents.add(eventObject);
    ILoggingEvent getLastLoggedEvent() {
        if (loggingEvents.isEmpty()) return null;
        return loggingEvents.get(loggingEvents.size() - 1);

TestLogAppender

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.slf4j.LoggerFactory;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class ControllerLoggerTestJava {
    @Test
    public void given_service_error_when_get_all_called_then_log_warn() throws Exception {
        ItemsService service = Mockito.mock(ItemsService.class);
        Mockito.when(service.getAll()).thenThrow(new Exception("Something failed :("));
        Controller controller = new Controller(service);
        TestLogAppender testLogAppender = new TestLogAppender();
        Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
        logger.addAppender(testLogAppender);
        testLogAppender.start();
        List<Entity> entityList = controller.getAll();
        ILoggingEvent lastLoggedEvent = testLogAppender.getLastLoggedEvent();
        assertNotNull(lastLoggedEvent);
        assertEquals(lastLoggedEvent.getMessage(), "Service.getAll returned exception instead of empty list");

The same test case as before, but in Java, with Junit5 and classic Mockito 

Summary

Best practices for Android Architecture in 2024 Android Pro | Architect Masterclass Series

Enroll to 3rd cohort of The Android Architect androidpro.io/architect

Read More »