JUnit test runtime for Scala.js - provides the runtime infrastructure for running JUnit tests compiled with Scala.js
—
The Scala.js JUnit runtime provides enhanced exception classes for detailed test failure reporting, including string comparison diffs and assumption violation handling. These exceptions integrate with the test framework to provide clear, actionable error messages.
class ComparisonFailure(message: String, expected: String, actual: String) extends AssertionError {
def getMessage(): String
def getExpected(): String
def getActual(): String
}
object ComparisonFailure {
// Factory methods and comparison utilities
}ComparisonFailure provides enhanced error reporting for string comparisons with automatic diff generation.
Basic Usage:
// Thrown automatically by assertEquals for strings
assertEquals("Hello World", "Hello Universe")
// Throws: ComparisonFailure with diff highlighting the differenceManual Usage:
def compareComplexStrings(expected: String, actual: String): Unit = {
if (expected != actual) {
throw new ComparisonFailure("String comparison failed", expected, actual)
}
}Error Message Format:
expected:<Hello [World]> but was:<Hello [Universe]>The brackets [] highlight the differing portions of the strings.
class AssumptionViolatedException extends RuntimeException {
// Multiple constructor overloads:
def this(message: String) = this()
def this(assumption: String, t: Throwable) = this()
def this(assumption: String, actual: Any, matcher: Matcher[_]) = this()
}Usage:
import org.junit.Assume._
@Test
def shouldRunOnLinux(): Unit = {
assumeTrue("Test requires Linux", System.getProperty("os.name").contains("Linux"))
// If not on Linux, throws AssumptionViolatedException -> test is skipped
}class TestCouldNotBeSkippedException(cause: internal.AssumptionViolatedException) extends RuntimeException {
// Exception when test cannot be skipped due to assumption failure
}This exception wraps internal assumption violations when tests cannot be properly skipped.
// Internal package version
class internal.AssumptionViolatedException extends RuntimeException with SelfDescribing {
def getMessage(): String
def describeTo(description: Description): Unit
// Enhanced with Hamcrest integration for detailed mismatch descriptions
}Constructor Variants:
// Internal version constructors
new internal.AssumptionViolatedException(message: String)
new internal.AssumptionViolatedException(message: String, cause: Throwable)
new internal.AssumptionViolatedException(assumption: String, value: Any, matcher: Matcher[_])class ArrayComparisonFailure(message: String, cause: AssertionError, index: Int) extends AssertionError {
def addDimension(index: Int): Unit
def getMessage(): String
override def toString(): String
}
object ArrayComparisonFailure {
// Factory methods for creating array-specific failures
}Provides detailed error reporting for array comparison failures with index information.
Example Error Messages:
// Single dimension array
val expected = Array(1, 2, 3)
val actual = Array(1, 5, 3)
assertArrayEquals(expected, actual)
// Error: "arrays first differed at element [1]; expected:<2> but was:<5>"
// Multi-dimensional array
val expected = Array(Array(1, 2), Array(3, 4))
val actual = Array(Array(1, 2), Array(3, 9))
assertArrayEquals(expected, actual)
// Error: "arrays first differed at element [1][1]; expected:<4> but was:<9>"class TestExecutor {
def executeTest(testMethod: Method, testInstance: Any): TestResult = {
try {
// Execute @Before methods
executeBeforeMethods(testInstance)
// Execute test method
testMethod.invoke(testInstance)
TestResult.Success
} catch {
// Test failures - assertion errors
case e: AssertionError =>
TestResult.Failure(e)
// Test errors - unexpected exceptions
case e: Exception =>
TestResult.Error(e)
// Assumption violations - skip test
case e: AssumptionViolatedException =>
TestResult.Skipped(e)
} finally {
// Always execute @After methods
try {
executeAfterMethods(testInstance)
} catch {
case e: Exception =>
// Log but don't fail test if @After fails
logAfterMethodFailure(e)
}
}
}
}class EnhancedExceptionReporter {
def analyzeException(e: Throwable): ExceptionReport = {
e match {
case cf: ComparisonFailure =>
ExceptionReport(
type = "String Comparison Failure",
message = cf.getMessage(),
expected = Some(cf.getExpected()),
actual = Some(cf.getActual()),
diff = generateDiff(cf.getExpected(), cf.getActual())
)
case acf: ArrayComparisonFailure =>
ExceptionReport(
type = "Array Comparison Failure",
message = acf.getMessage(),
index = Some(extractFailureIndex(acf)),
cause = Option(acf.getCause()).map(analyzeException)
)
case ave: AssumptionViolatedException =>
ExceptionReport(
type = "Assumption Violation",
message = ave.getMessage(),
skipped = true
)
case ae: AssertionError =>
ExceptionReport(
type = "Assertion Failure",
message = ae.getMessage(),
stackTrace = ae.getStackTrace()
)
}
}
}object CustomAsserts {
def assertJsonEquals(expected: String, actual: String): Unit = {
val expectedJson = parseJson(expected)
val actualJson = parseJson(actual)
if (expectedJson != actualJson) {
val prettyExpected = formatJson(expectedJson)
val prettyActual = formatJson(actualJson)
throw new ComparisonFailure("JSON comparison failed", prettyExpected, prettyActual)
}
}
def assertXmlEquals(expected: String, actual: String): Unit = {
val expectedXml = normalizeXml(expected)
val actualXml = normalizeXml(actual)
if (expectedXml != actualXml) {
throw new ComparisonFailure("XML comparison failed", expectedXml, actualXml)
}
}
}object CustomAssumptions {
def assumeNetworkAvailable(): Unit = {
try {
val socket = new Socket("www.google.com", 80)
socket.close()
} catch {
case _: IOException =>
throw new AssumptionViolatedException("Network connectivity required for this test")
}
}
def assumeMinimumJavaVersion(major: Int, minor: Int): Unit = {
val version = System.getProperty("java.version")
val parts = version.split("\\.")
val actualMajor = parts(0).toInt
val actualMinor = if (parts.length > 1) parts(1).toInt else 0
if (actualMajor < major || (actualMajor == major && actualMinor < minor)) {
throw new AssumptionViolatedException(
s"Java $major.$minor+ required, but running on $version"
)
}
}
}The Reporter class provides enhanced stack trace filtering for cleaner error output:
class Reporter {
private def logTrace(t: Throwable): Unit = {
val trace = t.getStackTrace.dropWhile { elem =>
elem.getFileName() != null && (
elem.getFileName().contains("StackTrace.scala") ||
elem.getFileName().contains("Throwables.scala")
)
}
val relevantTrace = trace.takeWhile { elem =>
!elem.toString.startsWith("org.junit.") &&
!elem.toString.startsWith("org.hamcrest.")
}
relevantTrace.foreach { elem =>
log(_.error, " at " + formatStackTraceElement(elem))
}
}
private def formatStackTraceElement(elem: StackTraceElement): String = {
val className = settings.decodeName(elem.getClassName())
val methodName = settings.decodeName(elem.getMethodName())
val location = if (elem.getFileName() != null && elem.getLineNumber() >= 0) {
s"${elem.getFileName()}:${elem.getLineNumber()}"
} else {
"Unknown Source"
}
s"$className.$methodName($location)"
}
}object ExceptionFormatter {
def formatAssertionError(message: String, expected: Any, actual: Any): String = {
val prefix = if (message != null && message.nonEmpty) s"$message " else ""
val expectedStr = String.valueOf(expected)
val actualStr = String.valueOf(actual)
if (expectedStr == actualStr) {
// Same string representation, show types
val expectedType = if (expected != null) expected.getClass.getName else "null"
val actualType = if (actual != null) actual.getClass.getName else "null"
s"${prefix}expected: $expectedType<$expectedStr> but was: $actualType<$actualStr>"
} else {
s"${prefix}expected:<$expectedStr> but was:<$actualStr>"
}
}
}Exception messages are formatted for optimal display in IDEs:
// SBT test output
[info] MyTest:
[info] - shouldCompareStrings *** FAILED ***
[info] expected:<Hello [World]> but was:<Hello [Universe]> (MyTest.scala:15)
[info] - shouldRunOnLinux *** SKIPPED ***
[info] Test requires Linux (MyTest.scala:20)Exception handling integrates with continuous integration:
// Good - uses ComparisonFailure automatically
assertEquals(expected, actual)
// Avoid - manual exception throwing unless needed
if (expected != actual) throw new AssertionError("Values differ")assertEquals("User name should match", expectedName, user.getName())
assertArrayEquals("Pixel values should be identical", expectedPixels, actualPixels)assumeTrue("Database connection required", databaseAvailable())
// Better than: if (!databaseAvailable()) return; // silently skiptry {
complexOperation()
} catch {
case e: SomeSpecificException =>
throw new AssertionError("Complex operation failed", e)
}Install with Tessl CLI
npx tessl i tessl/maven-org-scala-js--scalajs-junit-test-runtime