Comprehensive exception classes for test failures with IDE integration and detailed error reporting. MUnit provides specialized exception types that work well with development tools and provide actionable error messages.
The primary exception type for test failures with support for custom messages, causes, and location tracking.
/**
* Primary exception for test failures
* @param message The failure message describing what went wrong
* @param cause The underlying exception that caused the failure (optional)
* @param isStackTracesEnabled Whether to include stack traces in output
* @param location Source code location where the failure occurred
*/
class FailException(
val message: String,
val cause: Throwable,
val isStackTracesEnabled: Boolean,
val location: Location
) extends AssertionError(message, cause) with FailExceptionLike[FailException] {
/** Create a new exception with a different message */
def withMessage(newMessage: String): FailException
}Usage Examples:
class ExceptionExamples extends FunSuite {
test("explicit failure") {
val result = performOperation()
if (!result.isValid) {
throw new FailException(
s"Operation failed: ${result.error}",
result.cause,
true,
Location.empty
)
}
}
test("conditional failure with context") {
val data = processData()
if (data.isEmpty) {
fail("No data was processed") // This throws FailException internally
}
}
}Specialized exception for comparison failures that integrates with IDE diff viewers and provides structured comparison data.
/**
* Exception for comparison failures with IDE integration support
* Extends JUnit's ComparisonFailure for IDE compatibility
*/
class ComparisonFailException(
val message: String,
val obtained: Any,
val expected: Any,
val obtainedString: String,
val expectedString: String,
val location: Location
) extends ComparisonFailure(message, expectedString, obtainedString) with FailExceptionLike[ComparisonFailException] {
/** Create a new exception with a different message */
def withMessage(newMessage: String): ComparisonFailException
}Usage Examples:
This exception is typically thrown automatically by assertion methods like assertEquals, but can be used directly:
class ComparisonExamples extends FunSuite {
test("assertEquals throws ComparisonFailException") {
val obtained = List(1, 2, 3)
val expected = List(1, 2, 4)
// This will throw ComparisonFailException, which IDEs can display as a diff
assertEquals(obtained, expected)
}
test("manual comparison failure") {
val result = parseJson(input)
val expected = ExpectedResult(...)
if (result != expected) {
throw new ComparisonFailException(
"JSON parsing mismatch",
result,
expected,
result.toString,
expected.toString,
Location.empty
)
}
}
}Exception type for failures that should abort the entire test suite rather than just a single test.
/**
* Exception that aborts the entire test suite
* @param message The failure message explaining why the suite was aborted
* @param location Source code location where the suite failure occurred
*/
class FailSuiteException(
override val message: String,
override val location: Location
) extends FailException(message, location) {
/** Create a new exception with a different message */
def withMessage(newMessage: String): FailSuiteException
}Usage Examples:
class SuiteFailureExamples extends FunSuite {
override def beforeAll(): Unit = {
val dbConnection = connectToDatabase()
if (!dbConnection.isValid) {
failSuite("Cannot connect to test database")
// This throws FailSuiteException and stops all tests
}
}
test("this test won't run if suite failed") {
// This test is skipped if beforeAll() threw FailSuiteException
assert(true)
}
}Trait that provides common functionality for all MUnit failure exceptions.
/**
* Common behavior for all MUnit failure exceptions
* @tparam T The concrete exception type
*/
trait FailExceptionLike[T <: AssertionError] extends Serializable {
/** Create a new exception with a different message */
def withMessage(message: String): T
/** Transform the message using a function */
def updateMessage(f: String => String): T
/** Source code location where the failure occurred */
def location: Location
/** Whether stack traces are enabled for this exception */
def isStackTracesEnabled: Boolean
}Utility functions for working with exceptions and error handling.
/**
* Utilities for exception handling
*/
object Exceptions {
/**
* Find the root cause of a nested exception
* @param x The exception to unwrap
* @return The deepest cause in the exception chain
*/
def rootCause(x: Throwable): Throwable
}Usage Examples:
class ExceptionUtilityExamples extends FunSuite {
test("finding root cause") {
val rootCause = new IllegalArgumentException("Invalid input")
val wrapped = new RuntimeException("Processing failed", rootCause)
val nested = new Exception("Operation failed", wrapped)
val actual = Exceptions.rootCause(nested)
assertEquals(actual, rootCause)
assertEquals(actual.getMessage, "Invalid input")
}
}Interface for customizing how comparison failures are handled and reported.
/**
* Handler for customizing comparison failure behavior
*/
trait ComparisonFailExceptionHandler {
/**
* Handle a comparison failure with custom logic
* @param message The failure message
* @param obtained String representation of the obtained value
* @param expected String representation of the expected value
* @param location Source code location of the failure
* @return Never returns (always throws an exception)
*/
def handle(
message: String,
obtained: String,
expected: String,
location: Location
): Nothing
}Additional exception types for specific testing scenarios.
/**
* Exception for flaky test failures (internal use)
* Extends FailException but with NoStackTrace for cleaner output
*/
class FlakyFailure(error: Throwable) extends FailException with NoStackTrace with Serializableclass ErrorHandlingExamples extends FunSuite {
test("graceful failure with context") {
val input = getUserInput()
try {
val result = processInput(input)
assertEquals(result.status, "success")
} catch {
case e: ProcessingException =>
fail(
s"Processing failed for input: $input",
e
)
}
}
test("assertion with debugging context") {
val users = getUsers()
val activeUsers = users.filter(_.isActive)
assert(
activeUsers.nonEmpty,
s"Expected active users but got: ${users.map(u => s"${u.name}(${u.status})").mkString(", ")}"
)
}
}class DatabaseSuite extends FunSuite {
private var database: Database = _
override def beforeAll(): Unit = {
try {
database = Database.connect(testConfig)
database.migrate()
} catch {
case e: SQLException =>
failSuite(s"Failed to setup test database: ${e.getMessage}")
}
}
override def afterAll(): Unit = {
try {
database.close()
} catch {
case e: SQLException =>
// Log but don't fail suite during cleanup
println(s"Warning: Failed to close database: ${e.getMessage}")
}
}
}object CustomAssertions {
def assertValidEmail(email: String)(implicit loc: Location): Unit = {
if (!email.contains("@")) {
throw new FailException(
s"Invalid email format: '$email' (missing @)",
null,
true,
loc
)
}
}
def assertBetween[T: Ordering](value: T, min: T, max: T)(implicit loc: Location): Unit = {
val ord = implicitly[Ordering[T]]
if (ord.lt(value, min) || ord.gt(value, max)) {
throw new FailException(
s"Value $value is not between $min and $max",
null,
true,
loc
)
}
}
}
class CustomAssertionExamples extends FunSuite {
import CustomAssertions._
test("email validation") {
assertValidEmail("user@example.com")
}
test("range validation") {
val score = calculateScore()
assertBetween(score, 0.0, 100.0)
}
}