Home > Enterprise >  How to use mock in a private static final variable in Spock?
How to use mock in a private static final variable in Spock?

Time:02-25

Say I have a private static final variable in a Java class like:

@Service
public class MyClass {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyClass.class);
    ...
}

Then I have test class like:

class MyClassTest extends Specification {

  @Autowired
  MyClass sut

  def "Testing a private static final variable"() {
    given:
    sut.LOGGER = Mock(Logger)

    when:
    ...
}

How can I use Mock(Logger) with sut.LOGGER where LOGGER is a private static final in MyClass class? Without the final keyword, the test would just run fine though but I wanted it to work with a final keyword. Any suggestion is highly appreciated.

CodePudding user response:

Assuming that we do not wish to use Mockito inline mock maker or other mocking power tools, but simply use Spock's on-board means, we can use another way specifically for Slf4j: use the existing logger instead of mocking it, but adding a mock appender, then verify interactions on that one. The only requirement for that is to know which actual logging tool is used or to just configure the test environment in order to use a specific one. Let us choose LogBack. If we know that Slf4J logs to LogBack, we can import its classes and cast the logger to it.

Class under test:

I made it a bit more complicated on purpose, so later we can test several cases for positive, negative and zero numbers:

package de.scrum_master.stackoverflow.q71235231;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ClassWithLogger {
  private static final Logger LOGGER = LoggerFactory.getLogger(ClassWithLogger.class);

  public void logSomething(int i) {
    LOGGER.info("FYI, 3 * {} = {}", i, 3 * i);
    if (i < 0)
      LOGGER.warn("Be warned that {} is a negative number", i);
    if (i == 0)
      LOGGER.error("Using zero is forbidden");
  }
}

Spock test

package de.scrum_master.stackoverflow.q71235231

import ch.qos.logback.classic.Level
import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.spi.LoggingEvent
import ch.qos.logback.core.Appender
import spock.lang.Specification
import spock.lang.Unroll

class ClassWithLoggerTest extends Specification {
  @Unroll("test logger with number #number")
  def "test logger"() {
    given:
    // Groovy can simply access private fields, if we know their names
    Logger logger = ClassWithLogger.LOGGER
    def appender = Mock(Appender)
    logger.addAppender(appender)

    when:
    new ClassWithLogger().logSomething(number)

    then:
    1 * appender.doAppend({ LoggingEvent event ->
      event.level == Level.INFO &&
        event.formattedMessage == "FYI, 3 * $number = ${3 * number}"
    })
    (number == 0 ? 1 : 0) * appender.doAppend({ LoggingEvent event ->
      event.level == Level.ERROR &&
        event.formattedMessage == "Using zero is forbidden"
    })
    (number < 0 ? 1 : 0) * appender.doAppend({ LoggingEvent event ->
      event.level == Level.WARN &&
        event.formattedMessage == "Be warned that $number is a negative number"
    })

    cleanup:
    logger.detachAppender(appender)

    where:
    number << [4, 1, 0, -1, -7]
  }
}

The dynamic interaction counts like (number < 0 ? 1 : 0) * ... are not super readable. I simply wanted to show off Spock's capabilities in connection with test parametrisation and cover all scenarios in a single test method. Maybe you would be better off to split it into 3 methods and cover each case separately. That way the tests would be more readable, but you would have more duplicate code, too. It is a matter of taste, I leave the choice up to you.


Update: If you prefer to calculate more of the dynamic stuff in the where: section (could also be done in given:), you can use this variation (just cosmetics, the logic is unchanged):

class ClassWithLoggerTest extends Specification {
  @Unroll("test logger with number #number")
  def "test logger"() {
    given:
    // Groovy can simply access private fields, if we know their names
    Logger logger = ClassWithLogger.LOGGER
    def appender = Mock(Appender)
    logger.addAppender(appender)

    when:
    new ClassWithLogger().logSomething(number)

    then:
    infoCount * appender.doAppend({ LoggingEvent event ->
      event.level == Level.INFO && event.formattedMessage == infoMessage
    })
    errorCount * appender.doAppend({ LoggingEvent event ->
      event.level == Level.ERROR && event.formattedMessage == errorMessage
    })
    warningCount * appender.doAppend({ LoggingEvent event ->
      event.level == Level.WARN && event.formattedMessage == warningMessage
    })

    cleanup:
    logger.detachAppender(appender)

    where:
    number << [4, 1, 0, -1, -7]
    infoCount = 1
    infoMessage = "FYI, 3 * $number = ${3 * number}"
    errorCount = number == 0 ? 1 : 0
    errorMessage = "Using zero is forbidden"
    warningCount = number < 0 ? 1 : 0
    warningMessage = "Be warned that $number is a negative number"
  }
}

Source: blog post "Don't mock static: test SLF4J Logger with appenders", adapted to Spock.

  • Related