The Lift web framework integrates the SLF4J logging framework through a set of interfaces for performing logging and a configuration mechanism. The configuration mechanism attempts to configure the logging in a manner similar to the configuration for other parts of Lift. Unfortunately, this mechanism performs differently (or not at all) when running tests than it does when running normally. This post is a quick explanation of the configuration mechanism and how to configure logging during tests.
Logging Configuration (In Theory)
The configuration mechanism that Lift uses is documented on the Logging page on the Lift Wiki. This post presents only a simplified overview.
Programmatic Configuration
Logging can be configured programmatically in lift-webkit by assigning a
configuration function to LiftRules.configureLogging
(or directly to
Logger.setup
, to which LiftRules.configureLogging
delegates) before the
logging system is initialized. During initialization, the function will be called to configure the logging backend. Initialization is performed at most once,
when the first Logger
is created. So assigning a configuration function
after this point is useless.
Automatic Configuration
When using lift-webkit, configuration files ending with either .logback.xml
(when using Logback) or .log4j.xml
or
.log4j.props
(when using Log4J) are
found in the same way as Lift configuration properties files. For
example, the file src/main/resources/props/production.default.logback.xml
would be used in production mode on any server, if it existed.
This automatic configuration is accomplished by the function returned from
net.liftweb.util.LoggingAutoConfigurer.apply()
, which is the default value
of LiftRules.configureLogging
.
The Problem When Testing
Unfortunately, when testing (using
Specs2 or
ScalaTest with SBT),
the Automatic Configuration method is unlikely to work. The problem is that
if an instance of LiftRules
is not created before the first Logger
is
created, LoggingAutoConfigurer
will not be assigned to Logger.setup
before
setup is completed (and therefore it is never executed).
Solutions
Do Setup Before Each Test
All of the testing frameworks provide a mechanism for running code
before/after tests (or suites of tests). It’s quite possible to either create
an instance of LiftRules
, access a LiftRules
method on the LiftRules
object (which has an implicit conversion to LiftRules
), or assign
Logger.setup
directly through this mechanism. However, this requires the
most work and is therefore the worst solution (in my opinion).
Be warned, this method is very fragile. Because the order in which tests are
run is not deterministic, if any test creates a Logger
without performing
logging setup, it will prevent future configuration and cause all other tests
to log all messages in the default configuration.
Do Setup Before All Tests
In SBT, it is also possible to run code before any tests run by using
Tests.Setup and
Tests.Cleanup
in the testOptions
setting. This method is a bit awkward, since SBT project
code does not have access to the Lift classes directly (without adding a
dependency to the project code), so everything must be done via reflection.
To pass LoggingAutoConfigurer
to Logger.setup
, add the following to
build.sbt
(Note that blank lines would confuse the .sbt
parser, but would
be allowed in a .scala
file):
testOptions += Tests.Setup { loader: ClassLoader =>
// Get Logger.setup
val boxClass = loader.loadClass("net.liftweb.common.Box")
val loggerClass = loader.loadClass("net.liftweb.common.Logger$")
val logger = loggerClass.getField("MODULE$").get(null)
val loggerSetupEq = loggerClass.getMethod("setup_$eq", boxClass)
// Get function from LoggingAutoConfigurer.apply()
val configurerClass = loader.loadClass("net.liftweb.util.LoggingAutoConfigurer$")
val configurer = configurerClass.getField("MODULE$").get(null)
val configFunc = configurerClass.getMethod("apply").invoke(configurer)
// Put it in a Box
val fullClass = loader.loadClass("net.liftweb.common.Full")
val fullConstructor = fullClass.getConstructor(classOf[Object])
val configFuncBox = fullConstructor.newInstance(configFunc)
// Call Logger.setup on the Box
loggerSetupEq.invoke(logger, configFuncBox.asInstanceOf[Object])
}
Use The Default Search Path
If Logger.setup
has not been assigned, the logging backend will not be
configured by Lift. This does not mean that the logging backend will not be
configured at all. Conveniently, both Logback and Log4J, in their default
configurations, will search for configuration files on the classpath. Logback
will use logback-test.xml
or
logback.xml
and Log4J will
use log4j.properties
or the value of the log4j.configuration
system
property. Using
this fact, it is possible to configure Logback by creating
src/test/resources/logback.xml
(and similar for Log4J). This is by far the
easiest solution, if you don’t mind the asymmetry of the configuration file
locations.
Conclusion And A Note
It is possible to configure logging in Lift during tests using any of the above methods. My recommendation is to use either of the last two methods, based on whichever is more suitable for a given project.
In any case, be aware that in the default SBT configuration (fork := false
),
the JVM is shared between not only all tests, but all tasks invoked from SBT.
For this reason, re-running the test
task without exiting SBT will not
re-initialize logging. Also, running multiple tasks (such as test
and
run
) will break Lift, since the mode is determined only once.