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

exception-handling.mddocs/

Exception Handling

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.

Core Exception Classes

ComparisonFailure

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 difference

Manual 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.

AssumptionViolatedException

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
}

TestCouldNotBeSkippedException

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 Exception Handling

Internal AssumptionViolatedException

// 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[_])

ArrayComparisonFailure

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>"

Exception Flow and Handling

Test Execution Exception Flow

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)
      }
    }
  }
}

Exception Chaining and Root Cause Analysis

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()
        )
    }
  }
}

Custom Exception Creation

Creating Custom ComparisonFailure

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)
    }
  }
}

Custom Assumption Exceptions

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"
      )
    }
  }
}

Stack Trace Enhancement

Scala.js Stack Trace Filtering

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)"
  }
}

Exception Message Formatting

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>"
    }
  }
}

Integration with IDE and Build Tools

IDE Integration

Exception messages are formatted for optimal display in IDEs:

  • ComparisonFailure: IDEs can show side-by-side diff views
  • ArrayComparisonFailure: Click-to-navigate to specific array indices
  • AssumptionViolatedException: Marked as "skipped" rather than "failed"

Build Tool Integration

// 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)

CI/CD Integration

Exception handling integrates with continuous integration:

  • Failed tests: Return non-zero exit codes
  • Skipped tests: Don't fail builds but are reported
  • Error details: Captured in test reports (JUnit XML, etc.)

Best Practices

  1. Use Appropriate Exception Types: Let JUnit choose the right exception type automatically:
// Good - uses ComparisonFailure automatically
assertEquals(expected, actual)

// Avoid - manual exception throwing unless needed
if (expected != actual) throw new AssertionError("Values differ")
  1. Provide Meaningful Messages: Include context in assertion messages:
assertEquals("User name should match", expectedName, user.getName())
assertArrayEquals("Pixel values should be identical", expectedPixels, actualPixels)
  1. Handle Assumptions Properly: Use assumptions for environmental dependencies:
assumeTrue("Database connection required", databaseAvailable())
// Better than: if (!databaseAvailable()) return; // silently skip
  1. Chain Exceptions Appropriately: Preserve original exception context:
try {
  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

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