CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-scala-js--scalajs-junit-test-runtime

JUnit test runtime for Scala.js - provides the runtime infrastructure for running JUnit tests compiled with Scala.js

Pending
Overview
Eval results
Files

test-runners.mddocs/

Test Runners

JUnit provides a flexible runner framework that allows custom test execution strategies. The Scala.js implementation includes SBT testing framework integration and support for custom runners through annotations.

SBT Framework Integration

JUnitFramework

final class JUnitFramework extends Framework {
  val name: String = "Scala.js JUnit test framework"
  def fingerprints(): Array[Fingerprint]
  def runner(args: Array[String], remoteArgs: Array[String], testClassLoader: ClassLoader): Runner
  def slaveRunner(args: Array[String], remoteArgs: Array[String], testClassLoader: ClassLoader, send: String => Unit): Runner
}

The JUnitFramework class integrates JUnit tests with the SBT testing framework, enabling test discovery and execution in Scala.js projects.

Framework Properties:

val framework = new JUnitFramework()
println(framework.name) // "Scala.js JUnit test framework"

Test Discovery:

val fingerprints = framework.fingerprints()
// Returns array containing fingerprint for @org.junit.Test annotation

Runner Creation:

val args = Array("-v", "-s") // Verbose mode, decode Scala names
val runner = framework.runner(args, Array.empty, classLoader)

Test Discovery Fingerprint

trait Fingerprint {
  def requireNoArgConstructor(): Boolean
  def isModule(): Boolean
}

// JUnit-specific fingerprint
object JUnitFingerprint extends AnnotatedFingerprint {
  def annotationName(): String = "org.junit.Test"
  def isModule(): Boolean = false
}

Runner Hierarchy

Base Runner

abstract class Runner {
  def done(): String
  def tasks(taskDefs: Array[TaskDef]): Array[Task]
}

// Internal test metadata and execution support
trait Bootstrapper {
  def beforeClass(): Unit
  def afterClass(): Unit  
  def before(instance: AnyRef): Unit
  def after(instance: AnyRef): Unit
  def tests(): Array[TestMetadata]
  def invokeTest(instance: AnyRef, name: String): Future[Try[Unit]]
  def newInstance(): AnyRef
}

final class TestMetadata(val name: String, val ignored: Boolean, val annotation: org.junit.Test)

ParentRunner

abstract class ParentRunner[T](testClass: Class[_]) extends Runner {
  // Base implementation for runners that have child tests
}

BlockJUnit4ClassRunner

class BlockJUnit4ClassRunner(testClass: Class[_]) extends ParentRunner[FrameworkMethod] {
  // Standard JUnit 4 test class runner (dummy implementation for Scala.js)
}

JUnit4 Runner

final class JUnit4(klass: Class[_]) extends BlockJUnit4ClassRunner(klass) {
  // Default JUnit 4 runner
}

Custom Runner Support

@RunWith Annotation

class RunWith(value: Class[_ <: Runner]) extends StaticAnnotation with Annotation {
  def annotationType(): Class[_ <: Annotation] = classOf[RunWith]
}

Usage:

@RunWith(classOf[CustomTestRunner])
class SpecialTest {
  @Test
  def shouldRunWithCustomRunner(): Unit = {
    // This test will be executed by CustomTestRunner
    assertTrue(true)
  }
}

Parameterized Test Example:

@RunWith(classOf[ParameterizedRunner])
class ParameterizedTest(value: Int) {
  @Test
  def shouldTestWithParameter(): Unit = {
    assertTrue(s"Value $value should be positive", value > 0)
  }
}

Runner Configuration

Command Line Arguments

The JUnitFramework supports various command line arguments for test execution control:

val args = Array(
  "-v",  // Verbose output
  "-n",  // No color output  
  "-s",  // Decode Scala names
  "-a",  // Log assert messages
  "-c"   // Don't log exception class names
)

Argument Processing:

// Enable verbose mode
val verboseArgs = Array("-v")
val runner = framework.runner(verboseArgs, Array.empty, classLoader)

// Disable colored output for CI environments
val ciArgs = Array("-n")  
val ciRunner = framework.runner(ciArgs, Array.empty, classLoader)

RunSettings Configuration

case class RunSettings(
  color: Boolean,           // Enable colored output
  decodeScalaNames: Boolean, // Decode Scala names in stack traces
  verbose: Boolean,         // Verbose logging
  logAssert: Boolean,       // Log assertion failures
  notLogExceptionClass: Boolean // Don't log exception class names
)

Usage in Custom Runners:

class CustomRunner(args: Array[String], remoteArgs: Array[String], settings: RunSettings) {
  def executeTest(testMethod: Method): Unit = {
    if (settings.verbose) {
      println(s"Running test: ${settings.decodeName(testMethod.getName)}")
    }
    
    try {
      testMethod.invoke(testInstance)
      if (settings.verbose) {
        println("Test passed")
      }
    } catch {
      case e: AssertionError =>
        if (settings.logAssert) {
          println(s"Assertion failed: ${e.getMessage}")
        }
        throw e
    }
  }
}

Test Execution Flow

Task-Based Execution

trait Task {
  def taskDef(): TaskDef
  def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task]
}

trait TaskDef {
  def fullyQualifiedName(): String
  def fingerprint(): Fingerprint
  def explicitlySpecified(): Boolean
  def selectors(): Array[Selector]
}

Example Test Execution:

class JUnitTask(taskDef: TaskDef, settings: RunSettings) extends Task {
  def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task] = {
    val reporter = new Reporter(eventHandler, loggers, settings, taskDef)
    
    try {
      reporter.reportRunStarted()
      
      val testClass = Class.forName(taskDef.fullyQualifiedName())
      val testInstance = testClass.newInstance()
      
      // Execute @Before methods
      executeBeforeMethods(testInstance)
      
      // Execute test methods
      val testMethods = findTestMethods(testClass)
      testMethods.foreach(executeTestMethod(testInstance, _, reporter))
      
      // Execute @After methods
      executeAfterMethods(testInstance)
      
      reporter.reportRunFinished(failedCount, ignoredCount, totalCount, duration)
      
    } catch {
      case e: Exception =>
        reporter.reportErrors("Test execution failed", None, 0.0, List(e))
    }
    
    Array.empty // No subtasks
  }
}

Event Reporting

JUnitEvent

class JUnitEvent(
  taskDef: TaskDef,
  status: Status,
  selector: Selector,
  throwable: OptionalThrowable = new OptionalThrowable,
  duration: Long = -1L
) extends Event {
  def fullyQualifiedName(): String = taskDef.fullyQualifiedName()
  def fingerprint(): Fingerprint = taskDef.fingerprint()
}

Event Status Types:

  • Status.Success - Test passed
  • Status.Failure - Test failed with assertion error
  • Status.Error - Test failed with unexpected exception
  • Status.Skipped - Test was ignored or assumption failed

Reporter Integration

class Reporter(
  eventHandler: EventHandler,
  loggers: Array[Logger], 
  settings: RunSettings,
  taskDef: TaskDef
) {
  def reportTestStarted(method: String): Unit
  def reportTestFinished(method: String, succeeded: Boolean, timeInSeconds: Double): Unit
  def reportErrors(prefix: String, method: Option[String], timeInSeconds: Double, errors: List[Throwable]): Unit
  def reportIgnored(method: Option[String]): Unit
}

Custom Runner Implementation

class CustomParameterizedRunner(testClass: Class[_]) extends Runner {
  private val parameters = getParameters(testClass)
  
  def done(): String = ""
  
  def tasks(taskDefs: Array[TaskDef]): Array[Task] = {
    taskDefs.flatMap { taskDef =>
      parameters.map { param =>
        new ParameterizedTask(taskDef, param)
      }
    }
  }
  
  private def getParameters(clazz: Class[_]): Array[Any] = {
    // Extract parameters from @Parameters annotation or method
    Array(1, 2, 3, 4, 5) // Example parameters
  }
}

class ParameterizedTask(taskDef: TaskDef, parameter: Any) extends Task {
  def taskDef(): TaskDef = taskDef
  
  def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task] = {
    // Execute test with parameter
    val testClass = Class.forName(taskDef.fullyQualifiedName())
    val constructor = testClass.getConstructor(parameter.getClass)
    val testInstance = constructor.newInstance(parameter)
    
    // Execute test methods on parameterized instance
    executeTestMethods(testInstance, eventHandler, loggers)
    
    Array.empty
  }
}

Integration with Build Tools

SBT Integration

// build.sbt
libraryDependencies += "org.scala-js" %%% "scalajs-junit-test-runtime" % "1.19.0" % Test

// Enable JUnit testing
testFrameworks += new TestFramework("org.scalajs.junit.JUnitFramework")

Test Configuration

// Configure test execution
Test / testOptions += Tests.Argument(
  TestFrameworks.JUnit,
  "-v",      // Verbose
  "-s",      // Decode Scala names
  "-a"       // Log assertions
)

Advanced Runner Features

Parallel Execution Support

class ParallelJUnitRunner extends Runner {
  def tasks(taskDefs: Array[TaskDef]): Array[Task] = {
    taskDefs.map(new ParallelTask(_))
  }
}

class ParallelTask(taskDef: TaskDef) extends Task {
  def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task] = {
    // Execute test in parallel-safe manner
    Future {
      executeTestSafely(eventHandler, loggers)
    }
    Array.empty
  }
}

Test Filtering

class FilteringRunner(filter: String => Boolean) extends Runner {
  def tasks(taskDefs: Array[TaskDef]): Array[Task] = {
    taskDefs
      .filter(taskDef => filter(taskDef.fullyQualifiedName()))
      .map(new FilteredTask(_))
  }
}

The runner framework provides flexibility for custom test execution strategies while maintaining compatibility with standard JUnit test discovery and reporting mechanisms.

Install with Tessl CLI

npx tessl i tessl/maven-org-scala-js--scalajs-junit-test-runtime

docs

array-assertions.md

core-assertions.md

exception-handling.md

hamcrest-matchers.md

index.md

test-assumptions.md

test-lifecycle.md

test-runners.md

tile.json