Reputation: 477
I'm new to Scala and JVM,
I want to write unit test for logger but I get this error
when I ran sbt test
from terminal.
java.lang.ClassCastException: class org.apache.logging.slf4j.Log4jLogger cannot be cast to class ch.qos.logback.classic.Logger (org.apache.logging.slf4j.Log4jLogger and ch.qos.logback.classic.Logger are in unnamed module of loader 'app')
and this warning
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/Users/yl3/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/apache/logging/log4j/log4j-slf4j-impl/2.16.0/log4j-slf4j-impl-2.16.0.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/Users/yl3/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.2.11/logback-classic-1.2.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]
But when I ran test from IntelliJ, it passes.
My test code is below
import org.scalatest._
import flatspec._
import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.read.ListAppender
import org.slf4j.LoggerFactory
class MyAppSuite extends AnyFlatSpec {
it should "Log message" in {
object MyApp {
val LOGGER = LoggerFactory.getLogger(classOf[ILoggingEvent])
}
class MyApp {
def hello(word: String): Unit = {
MyApp.LOGGER.info(s"Word is ${word}")
}
}
val appender = new ListAppender[ILoggingEvent]
appender.start()
val logger = MyApp.LOGGER.asInstanceOf[Logger]
logger.addAppender(appender)
val myApp = new MyApp
myApp.hello("wow")
val logsList = appender.list
assert(logsList.size() === 1)
val logEvent = logsList.get(0)
assert(logEvent.getLevel.levelStr === "INFO")
assert(logEvent.getMessage === "Word is wow")
}
}
My dependencies in build.sbt
...
scalaVersion := "2.12.14"
val log4jVersion = "2.16.0"
val loggingDependencies = Seq(
"org.slf4j" % "slf4j-api" % "1.7.36",
"org.apache.logging.log4j" % "log4j-api" % log4jVersion,
"org.apache.logging.log4j" % "log4j-core" % log4jVersion,
"org.apache.logging.log4j" % "log4j-slf4j-impl" % log4jVersion
)
val testingDependencies = Seq(
"org.scalamock" %% "scalamock" % "5.2.0" % Test,
"org.scalatest" %% "scalatest" % "3.2.12" % Test,
"ch.qos.logback" % "logback-classic" % "1.2.11" % Test,
)
libraryDependencies ++= loggingDependencies ++ testingDependencies
excludeDependencies ++= Seq(
"org.slf4j" % "slf4j-log4j12"
)
When I ran test from IntelliJ I see this warning
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/Users/yl3/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.2.11/logback-classic-1.2.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/Users/yl3/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/apache/logging/log4j/log4j-slf4j-impl/2.16.0/log4j-slf4j-impl-2.16.0.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
I found similar logging tests in Java
So I assume it should work in Scala but not sure why casting doesn't work. Any suggestions or other ways to test logging using other libraries?
Upvotes: 0
Views: 2893
Reputation: 15105
The warning messages from SLF4J are really helpful. In both cases it says there are several logging libraries loaded in the classpath and this might cause trouble.
When looking in details, it even says which library is chosen as the SLF4J implementation:
Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]
This explains that your code that expects the logging library to be Logback works in IntelliJ tests but not with SBT run.
When there are several logging implementation in the classpath, there's no way to know for sure which one SLF4J will use.
Now, let's clarify the dependencies you have in your build definition:
slf4j-api
: the SlF4J API,log4j-api
: the log4j 2 API,log4j-core
: the log4j 2 implementationlog4j-slf4j-impl
: the binding to make SLF4J write logs with log4j 2 implementationlogback-core
and logback-classic
: Logback implementation (Logback comes with a binding to make SLF4J write logs with Logback automatically)
And you already excluded slf4j-log4j12
which is the binding to make SLF4J write logs with log4j 1.2 implementation.Solution
If you don't need several logging libraries (which is 99% of the time the case), just choose the implementation you want to use, it seems to be Logback as you have Logback specific code. Then remove other libraries implementation and binding dependencies.
This would mean to remove log4j-core
and log4j-slf4j-impl
in your case.
But then you might still have code that rely on the log4j 2 API to log stuff (other dependencies for instance). Thus you might need to add the binding library to make log4j 2 logs be written through SlF4J. To do so, add the log4j-to-slf4j-2.x
dependency.
Note: if for some strange reason you need several logging implementation, then do not assume in your code that it's one of them or handle it by handling cast exceptions.
Upvotes: 1