0
# Exception Handling
1
2
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.
3
4
## Core Exception Classes
5
6
### ComparisonFailure
7
8
```scala { .api }
9
class ComparisonFailure(message: String, expected: String, actual: String) extends AssertionError {
10
def getMessage(): String
11
def getExpected(): String
12
def getActual(): String
13
}
14
15
object ComparisonFailure {
16
// Factory methods and comparison utilities
17
}
18
```
19
20
ComparisonFailure provides enhanced error reporting for string comparisons with automatic diff generation.
21
22
**Basic Usage:**
23
```scala
24
// Thrown automatically by assertEquals for strings
25
assertEquals("Hello World", "Hello Universe")
26
// Throws: ComparisonFailure with diff highlighting the difference
27
```
28
29
**Manual Usage:**
30
```scala
31
def compareComplexStrings(expected: String, actual: String): Unit = {
32
if (expected != actual) {
33
throw new ComparisonFailure("String comparison failed", expected, actual)
34
}
35
}
36
```
37
38
**Error Message Format:**
39
```
40
expected:<Hello [World]> but was:<Hello [Universe]>
41
```
42
43
The brackets `[]` highlight the differing portions of the strings.
44
45
### AssumptionViolatedException
46
47
```scala { .api }
48
class AssumptionViolatedException extends RuntimeException {
49
// Multiple constructor overloads:
50
def this(message: String) = this()
51
def this(assumption: String, t: Throwable) = this()
52
def this(assumption: String, actual: Any, matcher: Matcher[_]) = this()
53
}
54
```
55
56
**Usage:**
57
```scala
58
import org.junit.Assume._
59
60
@Test
61
def shouldRunOnLinux(): Unit = {
62
assumeTrue("Test requires Linux", System.getProperty("os.name").contains("Linux"))
63
// If not on Linux, throws AssumptionViolatedException -> test is skipped
64
}
65
```
66
67
### TestCouldNotBeSkippedException
68
69
```scala { .api }
70
class TestCouldNotBeSkippedException(cause: internal.AssumptionViolatedException) extends RuntimeException {
71
// Exception when test cannot be skipped due to assumption failure
72
}
73
```
74
75
This exception wraps internal assumption violations when tests cannot be properly skipped.
76
77
## Internal Exception Handling
78
79
### Internal AssumptionViolatedException
80
81
```scala { .api }
82
// Internal package version
83
class internal.AssumptionViolatedException extends RuntimeException with SelfDescribing {
84
def getMessage(): String
85
def describeTo(description: Description): Unit
86
87
// Enhanced with Hamcrest integration for detailed mismatch descriptions
88
}
89
```
90
91
**Constructor Variants:**
92
```scala
93
// Internal version constructors
94
new internal.AssumptionViolatedException(message: String)
95
new internal.AssumptionViolatedException(message: String, cause: Throwable)
96
new internal.AssumptionViolatedException(assumption: String, value: Any, matcher: Matcher[_])
97
```
98
99
### ArrayComparisonFailure
100
101
```scala { .api }
102
class ArrayComparisonFailure(message: String, cause: AssertionError, index: Int) extends AssertionError {
103
def addDimension(index: Int): Unit
104
def getMessage(): String
105
override def toString(): String
106
}
107
108
object ArrayComparisonFailure {
109
// Factory methods for creating array-specific failures
110
}
111
```
112
113
Provides detailed error reporting for array comparison failures with index information.
114
115
**Example Error Messages:**
116
```scala
117
// Single dimension array
118
val expected = Array(1, 2, 3)
119
val actual = Array(1, 5, 3)
120
assertArrayEquals(expected, actual)
121
// Error: "arrays first differed at element [1]; expected:<2> but was:<5>"
122
123
// Multi-dimensional array
124
val expected = Array(Array(1, 2), Array(3, 4))
125
val actual = Array(Array(1, 2), Array(3, 9))
126
assertArrayEquals(expected, actual)
127
// Error: "arrays first differed at element [1][1]; expected:<4> but was:<9>"
128
```
129
130
## Exception Flow and Handling
131
132
### Test Execution Exception Flow
133
134
```scala
135
class TestExecutor {
136
def executeTest(testMethod: Method, testInstance: Any): TestResult = {
137
try {
138
// Execute @Before methods
139
executeBeforeMethods(testInstance)
140
141
// Execute test method
142
testMethod.invoke(testInstance)
143
144
TestResult.Success
145
146
} catch {
147
// Test failures - assertion errors
148
case e: AssertionError =>
149
TestResult.Failure(e)
150
151
// Test errors - unexpected exceptions
152
case e: Exception =>
153
TestResult.Error(e)
154
155
// Assumption violations - skip test
156
case e: AssumptionViolatedException =>
157
TestResult.Skipped(e)
158
159
} finally {
160
// Always execute @After methods
161
try {
162
executeAfterMethods(testInstance)
163
} catch {
164
case e: Exception =>
165
// Log but don't fail test if @After fails
166
logAfterMethodFailure(e)
167
}
168
}
169
}
170
}
171
```
172
173
### Exception Chaining and Root Cause Analysis
174
175
```scala
176
class EnhancedExceptionReporter {
177
def analyzeException(e: Throwable): ExceptionReport = {
178
e match {
179
case cf: ComparisonFailure =>
180
ExceptionReport(
181
type = "String Comparison Failure",
182
message = cf.getMessage(),
183
expected = Some(cf.getExpected()),
184
actual = Some(cf.getActual()),
185
diff = generateDiff(cf.getExpected(), cf.getActual())
186
)
187
188
case acf: ArrayComparisonFailure =>
189
ExceptionReport(
190
type = "Array Comparison Failure",
191
message = acf.getMessage(),
192
index = Some(extractFailureIndex(acf)),
193
cause = Option(acf.getCause()).map(analyzeException)
194
)
195
196
case ave: AssumptionViolatedException =>
197
ExceptionReport(
198
type = "Assumption Violation",
199
message = ave.getMessage(),
200
skipped = true
201
)
202
203
case ae: AssertionError =>
204
ExceptionReport(
205
type = "Assertion Failure",
206
message = ae.getMessage(),
207
stackTrace = ae.getStackTrace()
208
)
209
}
210
}
211
}
212
```
213
214
## Custom Exception Creation
215
216
### Creating Custom ComparisonFailure
217
218
```scala
219
object CustomAsserts {
220
def assertJsonEquals(expected: String, actual: String): Unit = {
221
val expectedJson = parseJson(expected)
222
val actualJson = parseJson(actual)
223
224
if (expectedJson != actualJson) {
225
val prettyExpected = formatJson(expectedJson)
226
val prettyActual = formatJson(actualJson)
227
throw new ComparisonFailure("JSON comparison failed", prettyExpected, prettyActual)
228
}
229
}
230
231
def assertXmlEquals(expected: String, actual: String): Unit = {
232
val expectedXml = normalizeXml(expected)
233
val actualXml = normalizeXml(actual)
234
235
if (expectedXml != actualXml) {
236
throw new ComparisonFailure("XML comparison failed", expectedXml, actualXml)
237
}
238
}
239
}
240
```
241
242
### Custom Assumption Exceptions
243
244
```scala
245
object CustomAssumptions {
246
def assumeNetworkAvailable(): Unit = {
247
try {
248
val socket = new Socket("www.google.com", 80)
249
socket.close()
250
} catch {
251
case _: IOException =>
252
throw new AssumptionViolatedException("Network connectivity required for this test")
253
}
254
}
255
256
def assumeMinimumJavaVersion(major: Int, minor: Int): Unit = {
257
val version = System.getProperty("java.version")
258
val parts = version.split("\\.")
259
val actualMajor = parts(0).toInt
260
val actualMinor = if (parts.length > 1) parts(1).toInt else 0
261
262
if (actualMajor < major || (actualMajor == major && actualMinor < minor)) {
263
throw new AssumptionViolatedException(
264
s"Java $major.$minor+ required, but running on $version"
265
)
266
}
267
}
268
}
269
```
270
271
## Stack Trace Enhancement
272
273
### Scala.js Stack Trace Filtering
274
275
The Reporter class provides enhanced stack trace filtering for cleaner error output:
276
277
```scala
278
class Reporter {
279
private def logTrace(t: Throwable): Unit = {
280
val trace = t.getStackTrace.dropWhile { elem =>
281
elem.getFileName() != null && (
282
elem.getFileName().contains("StackTrace.scala") ||
283
elem.getFileName().contains("Throwables.scala")
284
)
285
}
286
287
val relevantTrace = trace.takeWhile { elem =>
288
!elem.toString.startsWith("org.junit.") &&
289
!elem.toString.startsWith("org.hamcrest.")
290
}
291
292
relevantTrace.foreach { elem =>
293
log(_.error, " at " + formatStackTraceElement(elem))
294
}
295
}
296
297
private def formatStackTraceElement(elem: StackTraceElement): String = {
298
val className = settings.decodeName(elem.getClassName())
299
val methodName = settings.decodeName(elem.getMethodName())
300
val location = if (elem.getFileName() != null && elem.getLineNumber() >= 0) {
301
s"${elem.getFileName()}:${elem.getLineNumber()}"
302
} else {
303
"Unknown Source"
304
}
305
306
s"$className.$methodName($location)"
307
}
308
}
309
```
310
311
### Exception Message Formatting
312
313
```scala
314
object ExceptionFormatter {
315
def formatAssertionError(message: String, expected: Any, actual: Any): String = {
316
val prefix = if (message != null && message.nonEmpty) s"$message " else ""
317
val expectedStr = String.valueOf(expected)
318
val actualStr = String.valueOf(actual)
319
320
if (expectedStr == actualStr) {
321
// Same string representation, show types
322
val expectedType = if (expected != null) expected.getClass.getName else "null"
323
val actualType = if (actual != null) actual.getClass.getName else "null"
324
s"${prefix}expected: $expectedType<$expectedStr> but was: $actualType<$actualStr>"
325
} else {
326
s"${prefix}expected:<$expectedStr> but was:<$actualStr>"
327
}
328
}
329
}
330
```
331
332
## Integration with IDE and Build Tools
333
334
### IDE Integration
335
336
Exception messages are formatted for optimal display in IDEs:
337
338
- **ComparisonFailure**: IDEs can show side-by-side diff views
339
- **ArrayComparisonFailure**: Click-to-navigate to specific array indices
340
- **AssumptionViolatedException**: Marked as "skipped" rather than "failed"
341
342
### Build Tool Integration
343
344
```scala
345
// SBT test output
346
[info] MyTest:
347
[info] - shouldCompareStrings *** FAILED ***
348
[info] expected:<Hello [World]> but was:<Hello [Universe]> (MyTest.scala:15)
349
[info] - shouldRunOnLinux *** SKIPPED ***
350
[info] Test requires Linux (MyTest.scala:20)
351
```
352
353
### CI/CD Integration
354
355
Exception handling integrates with continuous integration:
356
357
- **Failed tests**: Return non-zero exit codes
358
- **Skipped tests**: Don't fail builds but are reported
359
- **Error details**: Captured in test reports (JUnit XML, etc.)
360
361
## Best Practices
362
363
1. **Use Appropriate Exception Types**: Let JUnit choose the right exception type automatically:
364
```scala
365
// Good - uses ComparisonFailure automatically
366
assertEquals(expected, actual)
367
368
// Avoid - manual exception throwing unless needed
369
if (expected != actual) throw new AssertionError("Values differ")
370
```
371
372
2. **Provide Meaningful Messages**: Include context in assertion messages:
373
```scala
374
assertEquals("User name should match", expectedName, user.getName())
375
assertArrayEquals("Pixel values should be identical", expectedPixels, actualPixels)
376
```
377
378
3. **Handle Assumptions Properly**: Use assumptions for environmental dependencies:
379
```scala
380
assumeTrue("Database connection required", databaseAvailable())
381
// Better than: if (!databaseAvailable()) return; // silently skip
382
```
383
384
4. **Chain Exceptions Appropriately**: Preserve original exception context:
385
```scala
386
try {
387
complexOperation()
388
} catch {
389
case e: SomeSpecificException =>
390
throw new AssertionError("Complex operation failed", e)
391
}
392
```