11

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

3 Answers 3

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

Sign up to request clarification or add additional context in comments.

6 Comments

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?
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)
@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.
How does your car instance have access to the logger? isn't it private?
@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)
|
9

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

3 Comments

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!
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 stackoverflow.com/a/70350184/10592946)
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.
2

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.