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