You cannot override log
field, because it's a final static field. You see this
groovy.lang.MissingMethodException: No signature of method: core.ConsoleHandler.setLog() applicable for argument types: (ch.qos.logback.classic.Logger) (...)
exception, because final fields don't have any setter method. If it was not a final field you could try overriding it with:
ch.@log = Mock(Logger)
@
in this case means that you want to access object field directly (Groovy compiles ch.log
to ch.getLog()
when accessing value and ch.setLog()
when modifying field).
In general, you shouldn't test if logger logged any message inside the feature you are testing. Basically because it is out of scope of your current unit under test and if it comes to what your method returns - it doesn't matter if anything was logged or not. Also you don't even know if ERROR
level is enabled or not - it means that your test won't recognize if anything was actually logged to an appender or not. And secondly, at some point of time you can add another log.error()
to the method you test - it does not change anything to what your class or method provides, but the unit test starts failing, because you assumed that there is a single invocation of log.error()
.
If you are not convinced by those arguments, you can apply a hack to your test. You can't mock ch.log
field, but if you take a look what class it instantiates (org.slf4j.impl.SimpleLogger
) and what log.error()
calls in the end, you can find out that it gets PrintStream
object from:
CONFIG_PARAMS.outputChoice.getTargetPrintStream()
And because CONFIG_PARAMS.outputChoice
is not a final field, you can replace it with a mock. You still can't check if log.error()
was invoked, however you can check if mocked PrintStream
invoked .println(String str)
method n-number of times. This is very ugly solution, because it relies on internal implementation details of a org.slf4j.impl.SimpleLogger
class. I would call such workaround as an asking yourself for a problems, because you couple your test tightly to current org.slf4j.impl.SimpleLogger
implementation - it's pretty easy to imagine that a few months later you update Slf4j to a version that changes implementation of log.error()
and your test starts failing with no strategic reason. Here is what this dirty workaround looks like:
import groovy.util.logging.Slf4j
import org.slf4j.impl.OutputChoice
import spock.lang.Specification
class SpyMethodArgsExampleSpec extends Specification {
def "testing logging activity, but why?"() {
given:
ConsoleHandler ch = Spy(ConsoleHandler)
PrintStream printStream = Mock(PrintStream)
ch.log.CONFIG_PARAMS.outputChoice = Mock(OutputChoice)
ch.log.CONFIG_PARAMS.outputChoice.getTargetPrintStream() >> printStream
when:
ch.run()
then:
1 * printStream.println(_ as String)
}
@Slf4j
static class ConsoleHandler {
void run() {
log.error("test")
}
}
}
However I hope you won't go that way.
Update: making logging/reporting an important part of the feature we are implementing
Assuming that logging/reporting part is crucial to your class, it's worth rethinking your class design in this case. Defining class dependencies as a constructor parameters is a good practice - you explicitly express class dependencies at initialization level. Using @Slf4j
is very convenient way to add a static final logger, but in this case it's an implementation level detail and it's not visible from public client perspective. That's why testing such internal details is very tricky.
However, if logging is important to your class and you want to test interactions between class under test and its dependencies, there is nothing wrong in skipping @Slf4j
annotation and providing logger as a constructor parameter:
class ConsoleHandler {
private final Logger logger
ConsoleHandler(Logger logger) {
this.logger = logger
}
}
Of course it has downsides as well - you need to pass it anytime you create an instance of ConsoleHandler
class. But it makes it fully testable - in your test you simply mock Logger
instance and you are ready to go. But it makes sense only if testing those interactions makes sense from business perspective and those invocations are mandatory to fulfill the contract with the class you are testing. Otherwise it doesn't make much sense.