How to unit test logging error with Spock framework in groovy
Asked Answered
S

3

9

So I have a class that has a method that logs a message:

class Car {
    private Logger logger = LoggerFactory.getLogger(Car.class);


    void startCar() {
        logger.error("car stopped working");
    }
}

How can I test that the error was logged using the spock testing framework?

class CarTest extends Specification {
    def "test startCar"() {
        given:
        Car newCar = new Car();

        when:
        newCar.startCar();

        then:
        // HOW CAN I ASSERT THAT THE MESSAGE WAS LOGGED???
    }
}
Subtraction answered 26/6, 2014 at 19:58 Comment(0)
S
17

you could check for an invocation of error on the logger

@Grab(group='org.spockframework', module='spock-core', version='0.7-groovy-2.0')
@Grab(group='org.slf4j', module='slf4j-api', version='1.7.7')
@Grab(group='ch.qos.logback', module='logback-classic', version='1.1.2')

import org.slf4j.Logger

class MockLog extends spock.lang.Specification {

    public class Car {
        private Logger logger = org.slf4j.LoggerFactory.getLogger(Car.class);
        void startCar() {
            logger.error('car stopped working');
        }
    }

    def "mock log"() {
    given:
        def car = new Car()
        car.logger = Mock(Logger)
    when:
        car.startCar()
    then:
        1 * car.logger.error('car stopped working')
    }
}

edit: Full example https://github.com/christoph-frick/spock-test-logging

Shamekashameless answered 26/6, 2014 at 22:1 Comment(6)
groovy.lang.ReadOnlyPropertyException: Cannot set readonly property: LOG . In your example you have access to car.logger - but how to this if the class is not within the Spock Testclass?Educatory
This example seems misleading. In a real life scenario, your logger would not be acccessible to your test code (class Car would not be inside the Specification)Eldaelden
@mattfreake I added a link to a full example to show, that this does not rely on the class to be inside the test. This was done to have a short example. If you mean something else, please elaborate. If you are against writing tests like this, then I am with you - but please blame OP and not the answer. If you need something so badly logged, that it merits writing a test, then provide a proper infrastructure for it. If the test is hairy or hard to write, it's not the tests fault but the fault of the SUT.Shamekashameless
How does your car instance have access to the logger? isn't it private?Have
@L.Dai Groovy and/or Spock don't care. Also private on the jvm is not really private (can at least be accessed via reflection)Shamekashameless
What if you dont have Logger in the class as constructor dependency? If there is @Slf4j from lombok then?Giza
A
8

My Loggers are private static final so I cannot use solution mentioned above and rather not use Reflection.

If you are using Spring, you have acces to OutputCaptureRule.

@Rule
OutputCaptureRule outputCaptureRule = new OutputCaptureRule()

def test(){
outputCaptureRule.getAll().contains("<your test output>")
}
Andres answered 17/4, 2020 at 9:52 Comment(3)
Not only does this work with Spring, but it also works with Travis CI. Previously, OutputCapture was the class I would use in Spring/Spock, but it was deprecated and would fail the CI build. I'm glad I searched for another answer and found this! Also, I feel I should mention we're using @Slf4j, and this is perfect. Thanks!Xerophilous
You may also need to add dependency 'org.spockframework:spock-junit4:2.0-groovy-3.0' and also set the parameter follow="true" for your Console appender in the log4j2 configuration file (see https://mcmap.net/q/1170811/-ouputcapture-with-multiple-tests)Theme
Some important notes: 1 - the field must be public, or public static if it is a @ClassRule 2 - This is for Junit4, if you are using JUnit5 (which you very likely are if you are on springboot 2.6+) you need to use OutputCaptureExtension instead as JUnit5 ignores @Rules altogether. Took me a while to figure this out.Maudmaude
Q
0

Your class has a logger field marked as final, which prevents it from being modified during testing (You'll have the same thing when you use @Slf4j). In this case, you can solve the problem in a different way, such as using unit tests and System.out to capture logs.

class TestAppender extends AppenderBase<ILoggingEvent> {
    List<ILoggingEvent> events = []

    @Override
    protected void append(ILoggingEvent eventObject) {
        events.add(eventObject)
    }
}

Some explanation about things what I did:

  • TestAppender: Implementation of your own Appender that stores logs in the events list.
  • setup and cleanup: Adding and removing our TestAppender to/from the logger before and after each test.
  • testAppender.events.any { ... }: Verify that appropriate login messages have been logged.

These tests verify that the appropriate log messages are invoked when class methods are executed.

import ch.qos.logback.classic.Logger
import org.slf4j.LoggerFactory
import spock.lang.Specification
import spock.lang.Subject
import spock.lang.Unroll

class YourClassSpec extends Specification {

    @Subject
    YourClass yourClass = new YourClass()

    Logger log = (Logger) LoggerFactory.getLogger(YourClass)

    TestAppender testAppender = new TestAppender()

    def setup() {
        testAppender.start()
        log.addAppender(testAppender)
    }

    def cleanup() {
        log.detachAppender(testAppender)
    }

    @Unroll
    def "Test values with userEmail: #userEmail and groupName: #groupName"() {
        given:
        String userEmail = "[email protected]"
        String groupName = "testgroup"

        when:
        yourClass.testMethod(userEmail, groupName)

        then:
        testAppender.events.any { it.message == "Here you can compare values and logs: {} {}" && it.argumentArray[0] == userEmail && it.argumentArray[1] == groupName }
    }
}
Quirinus answered 13/6 at 10:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.