Skip to content

Commit ca23bbc

Browse files
committed
scalatestsuite: Collect and output logs for failing tests
This patch introduces a new feature of the `UnitTestSuite`: When tests are ran we collect logs for the `no.ndla` package in a buffer, and when a test fails the collected logs are printed for that test in particular. This allows us to keep clean and readable logs when tests are green, but will allow us to more easily find mistakes when a test fails.
1 parent bcc5be2 commit ca23bbc

File tree

10 files changed

+174
-13
lines changed

10 files changed

+174
-13
lines changed

common/src/main/scala/no/ndla/common/model/NDLADate.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ case class NDLADate(underlying: ZonedDateTime) extends Ordered[NDLADate] {
5555
}
5656
}
5757

58-
object NDLADate {
58+
object NDLADate extends StrictLogging {
5959

6060
implicit val typescriptType: TSType[NDLADate] = TSType.sameAs[NDLADate, String]
6161

draft-api/src/test/scala/no/ndla/draftapi/integration/TaxonomyApiClientTest.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class TaxonomyApiClientTest extends UnitSuite with TestEnvironment {
2222

2323
override val taxonomyApiClient: TaxonomyApiClient = spy(new TaxonomyApiClient)
2424

25-
override protected def beforeEach(): Unit = {
25+
override def beforeEach(): Unit = {
2626
// Since we use spy, we reset the mock before each test allowing verify to be accurate
2727
reset(taxonomyApiClient)
2828
}

log4j2-test.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
Configuration:
22
status: warn
33
Loggers:
4+
Logger:
5+
name: "no.ndla"
6+
level: debug
47
Root:
58
level: warn

project/scalatestsuitelib.scala

+11-6
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@ import sbt.Keys.*
66
object scalatestsuitelib extends Module {
77
override val moduleName: String = "scalatestsuite"
88
override val enableReleases: Boolean = false
9-
lazy val dependencies: Seq[ModuleID] = Seq(
10-
"org.scalatest" %% "scalatest" % ScalaTestV,
11-
"org.testcontainers" % "elasticsearch" % TestContainersV,
12-
"org.testcontainers" % "testcontainers" % TestContainersV,
13-
"org.testcontainers" % "postgresql" % TestContainersV
14-
) ++ database ++ vulnerabilityOverrides ++ mockito
9+
lazy val dependencies: Seq[ModuleID] = withLogging(
10+
Seq(
11+
"org.scalatest" %% "scalatest" % ScalaTestV,
12+
"org.testcontainers" % "elasticsearch" % TestContainersV,
13+
"org.testcontainers" % "testcontainers" % TestContainersV,
14+
"org.testcontainers" % "postgresql" % TestContainersV
15+
),
16+
database,
17+
vulnerabilityOverrides,
18+
mockito
19+
)
1520

1621
override lazy val settings: Seq[Def.Setting[?]] = Seq(
1722
libraryDependencies ++= dependencies
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Part of NDLA backend.scalatestsuite.main
3+
* Copyright (C) 2025 NDLA
4+
*
5+
* See LICENSE
6+
*
7+
*/
8+
9+
package no.ndla.scalatestsuite
10+
11+
import org.apache.logging.log4j.core.LogEvent
12+
import org.apache.logging.log4j.core.appender.AbstractAppender
13+
import org.apache.logging.log4j.core.layout.PatternLayout
14+
15+
import scala.collection.mutable.ListBuffer
16+
17+
object Layout {
18+
lazy val prebuiltLayout: PatternLayout = PatternLayout
19+
.newBuilder()
20+
.withPattern("[%level] %C.%M#%L: %msg%n")
21+
.build()
22+
}
23+
24+
class BufferedLogAppender
25+
extends AbstractAppender("BufferedAppender", null, Layout.prebuiltLayout, false, Array.empty) {
26+
private val logQueue = ListBuffer.empty[String]
27+
def clear(): Unit = logQueue.clear()
28+
def printLogs(): Unit = logQueue.foreach(print)
29+
def getAndClearLogs: List[String] = {
30+
val toReturn = logQueue.toList
31+
logQueue.clear()
32+
toReturn
33+
}
34+
35+
override def append(event: LogEvent): Unit = {
36+
val logString = getLayout.toSerializable(event).toString
37+
logQueue.append(logString)
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Part of NDLA scalatestsuite
3+
* Copyright (C) 2025 NDLA
4+
*
5+
* See LICENSE
6+
*
7+
*/
8+
9+
package no.ndla.scalatestsuite
10+
11+
object ColoredText {
12+
val RED = "\u001b[31m"
13+
val GREEN = "\u001b[32m"
14+
val YELLOW = "\u001b[33m"
15+
val BLUE = "\u001b[34m"
16+
val RESET = "\u001b[0m"
17+
18+
def print(color: Colors, text: String) = {
19+
color match {
20+
case Red => println(s"$RED$text$RESET")
21+
case Green => println(s"$GREEN$text$RESET")
22+
case Yellow => println(s"$YELLOW$text$RESET")
23+
case Blue => println(s"$BLUE$text$RESET")
24+
}
25+
}
26+
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Part of NDLA scalatestsuite
3+
* Copyright (C) 2025 NDLA
4+
*
5+
* See LICENSE
6+
*
7+
*/
8+
9+
package no.ndla.scalatestsuite
10+
11+
sealed trait Colors
12+
case object Red extends Colors
13+
case object Green extends Colors
14+
case object Yellow extends Colors
15+
case object Blue extends Colors

scalatestsuite/src/main/scala/no/ndla/scalatestsuite/IntegrationSuite.scala

+6-1
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,13 @@ abstract class IntegrationSuite(
149149
})
150150
}
151151

152-
override def beforeAll(): Unit = setDatabaseEnvironment()
152+
override def beforeAll(): Unit = {
153+
super.beforeAll()
154+
setDatabaseEnvironment()
155+
}
156+
153157
override def afterAll(): Unit = {
158+
super.afterAll()
154159
setPropEnv(previousDatabaseEnv)
155160
elasticSearchContainer.foreach(c => c.stop())
156161
postgresContainer.foreach(c => c.stop())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Part of NDLA backend.scalatestsuite.main
3+
* Copyright (C) 2025 NDLA
4+
*
5+
* See LICENSE
6+
*
7+
*/
8+
9+
package no.ndla.scalatestsuite
10+
11+
import org.apache.logging.log4j.core.LoggerContext
12+
import org.apache.logging.log4j.{Level, LogManager}
13+
import org.scalatest.funsuite.AnyFunSuite
14+
import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Outcome}
15+
16+
import scala.collection.mutable.ListBuffer
17+
18+
/** Sets up test environment to keep logs and print them if the test fails */
19+
trait TestSuiteLoggingSetup extends AnyFunSuite with BeforeAndAfterEach with BeforeAndAfterAll {
20+
private val appender = new BufferedLogAppender()
21+
private def getNDLALogger = LogManager.getContext(false).asInstanceOf[LoggerContext].getLogger("no.ndla")
22+
private val beforeAllLog = ListBuffer.empty[String]
23+
24+
private def setupLogger(): Unit = {
25+
if (!appender.isStarted) appender.start()
26+
val ndlaLogger = getNDLALogger
27+
ndlaLogger.setLevel(Level.DEBUG)
28+
ndlaLogger.addAppender(appender)
29+
}
30+
31+
override def beforeEach(): Unit = {
32+
val logs = appender.getAndClearLogs
33+
beforeAllLog.addAll(logs)
34+
setupLogger()
35+
super.beforeEach()
36+
}
37+
38+
override def beforeAll(): Unit = {
39+
setupLogger()
40+
super.beforeAll()
41+
}
42+
43+
override def afterEach(): Unit = {
44+
val ndlaLogger = getNDLALogger
45+
ndlaLogger.removeAppender(appender)
46+
appender.clear()
47+
super.afterEach()
48+
}
49+
50+
override def withFixture(test: NoArgTest): Outcome = {
51+
val result = super.withFixture(test)
52+
if (!result.isSucceeded) {
53+
// If test fails, print the buffered logs
54+
val testName = s"${this.suiteName}$$'${test.name}'"
55+
ColoredText.print(Red, s"\n---- Captured Logs for test: $testName ----")
56+
if (beforeAllLog.nonEmpty) {
57+
ColoredText.print(Red, s">>> Captured Logs from $suiteName$$beforeAll: >>>")
58+
beforeAllLog.foreach(print)
59+
ColoredText.print(Red, s"<<< End of Captured Logs from $suiteName$$beforeAll <<<")
60+
}
61+
appender.printLogs()
62+
ColoredText.print(Red, s"---- End of Captured Logs for test: $testName ----\n")
63+
}
64+
result
65+
}
66+
}

scalatestsuite/src/main/scala/no/ndla/scalatestsuite/UnitTestSuite.scala

+5-4
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77

88
package no.ndla.scalatestsuite
99

10-
import org.scalatestplus.mockito.MockitoSugar
11-
import org.scalatest._
10+
import org.scalatest.*
1211
import org.scalatest.funsuite.AnyFunSuite
1312
import org.scalatest.matchers.should.Matchers
13+
import org.scalatestplus.mockito.MockitoSugar
1414

1515
import java.io.IOException
1616
import java.net.ServerSocket
1717
import scala.util.Properties.{propOrNone, setProp}
18-
import scala.util.{Try, Success, Failure}
18+
import scala.util.{Failure, Success, Try}
1919

2020
abstract class UnitTestSuite
2121
extends AnyFunSuite
@@ -25,7 +25,8 @@ abstract class UnitTestSuite
2525
with Inspectors
2626
with MockitoSugar
2727
with BeforeAndAfterEach
28-
with BeforeAndAfterAll {
28+
with BeforeAndAfterAll
29+
with TestSuiteLoggingSetup {
2930

3031
def setPropEnv(key: String, value: String): String = setProp(key, value)
3132

0 commit comments

Comments
 (0)