0
# Test Runners
1
2
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.
3
4
## SBT Framework Integration
5
6
### JUnitFramework
7
8
```scala { .api }
9
final class JUnitFramework extends Framework {
10
val name: String = "Scala.js JUnit test framework"
11
def fingerprints(): Array[Fingerprint]
12
def runner(args: Array[String], remoteArgs: Array[String], testClassLoader: ClassLoader): Runner
13
def slaveRunner(args: Array[String], remoteArgs: Array[String], testClassLoader: ClassLoader, send: String => Unit): Runner
14
}
15
```
16
17
The JUnitFramework class integrates JUnit tests with the SBT testing framework, enabling test discovery and execution in Scala.js projects.
18
19
**Framework Properties:**
20
```scala
21
val framework = new JUnitFramework()
22
println(framework.name) // "Scala.js JUnit test framework"
23
```
24
25
**Test Discovery:**
26
```scala
27
val fingerprints = framework.fingerprints()
28
// Returns array containing fingerprint for @org.junit.Test annotation
29
```
30
31
**Runner Creation:**
32
```scala
33
val args = Array("-v", "-s") // Verbose mode, decode Scala names
34
val runner = framework.runner(args, Array.empty, classLoader)
35
```
36
37
### Test Discovery Fingerprint
38
39
```scala { .api }
40
trait Fingerprint {
41
def requireNoArgConstructor(): Boolean
42
def isModule(): Boolean
43
}
44
45
// JUnit-specific fingerprint
46
object JUnitFingerprint extends AnnotatedFingerprint {
47
def annotationName(): String = "org.junit.Test"
48
def isModule(): Boolean = false
49
}
50
```
51
52
## Runner Hierarchy
53
54
### Base Runner
55
56
```scala { .api }
57
abstract class Runner {
58
def done(): String
59
def tasks(taskDefs: Array[TaskDef]): Array[Task]
60
}
61
62
// Internal test metadata and execution support
63
trait Bootstrapper {
64
def beforeClass(): Unit
65
def afterClass(): Unit
66
def before(instance: AnyRef): Unit
67
def after(instance: AnyRef): Unit
68
def tests(): Array[TestMetadata]
69
def invokeTest(instance: AnyRef, name: String): Future[Try[Unit]]
70
def newInstance(): AnyRef
71
}
72
73
final class TestMetadata(val name: String, val ignored: Boolean, val annotation: org.junit.Test)
74
```
75
76
### ParentRunner
77
78
```scala { .api }
79
abstract class ParentRunner[T](testClass: Class[_]) extends Runner {
80
// Base implementation for runners that have child tests
81
}
82
```
83
84
### BlockJUnit4ClassRunner
85
86
```scala { .api }
87
class BlockJUnit4ClassRunner(testClass: Class[_]) extends ParentRunner[FrameworkMethod] {
88
// Standard JUnit 4 test class runner (dummy implementation for Scala.js)
89
}
90
```
91
92
### JUnit4 Runner
93
94
```scala { .api }
95
final class JUnit4(klass: Class[_]) extends BlockJUnit4ClassRunner(klass) {
96
// Default JUnit 4 runner
97
}
98
```
99
100
## Custom Runner Support
101
102
### @RunWith Annotation
103
104
```scala { .api }
105
class RunWith(value: Class[_ <: Runner]) extends StaticAnnotation with Annotation {
106
def annotationType(): Class[_ <: Annotation] = classOf[RunWith]
107
}
108
```
109
110
**Usage:**
111
```scala
112
@RunWith(classOf[CustomTestRunner])
113
class SpecialTest {
114
@Test
115
def shouldRunWithCustomRunner(): Unit = {
116
// This test will be executed by CustomTestRunner
117
assertTrue(true)
118
}
119
}
120
```
121
122
**Parameterized Test Example:**
123
```scala
124
@RunWith(classOf[ParameterizedRunner])
125
class ParameterizedTest(value: Int) {
126
@Test
127
def shouldTestWithParameter(): Unit = {
128
assertTrue(s"Value $value should be positive", value > 0)
129
}
130
}
131
```
132
133
## Runner Configuration
134
135
### Command Line Arguments
136
137
The JUnitFramework supports various command line arguments for test execution control:
138
139
```scala
140
val args = Array(
141
"-v", // Verbose output
142
"-n", // No color output
143
"-s", // Decode Scala names
144
"-a", // Log assert messages
145
"-c" // Don't log exception class names
146
)
147
```
148
149
**Argument Processing:**
150
```scala
151
// Enable verbose mode
152
val verboseArgs = Array("-v")
153
val runner = framework.runner(verboseArgs, Array.empty, classLoader)
154
155
// Disable colored output for CI environments
156
val ciArgs = Array("-n")
157
val ciRunner = framework.runner(ciArgs, Array.empty, classLoader)
158
```
159
160
### RunSettings Configuration
161
162
```scala { .api }
163
case class RunSettings(
164
color: Boolean, // Enable colored output
165
decodeScalaNames: Boolean, // Decode Scala names in stack traces
166
verbose: Boolean, // Verbose logging
167
logAssert: Boolean, // Log assertion failures
168
notLogExceptionClass: Boolean // Don't log exception class names
169
)
170
```
171
172
**Usage in Custom Runners:**
173
```scala
174
class CustomRunner(args: Array[String], remoteArgs: Array[String], settings: RunSettings) {
175
def executeTest(testMethod: Method): Unit = {
176
if (settings.verbose) {
177
println(s"Running test: ${settings.decodeName(testMethod.getName)}")
178
}
179
180
try {
181
testMethod.invoke(testInstance)
182
if (settings.verbose) {
183
println("Test passed")
184
}
185
} catch {
186
case e: AssertionError =>
187
if (settings.logAssert) {
188
println(s"Assertion failed: ${e.getMessage}")
189
}
190
throw e
191
}
192
}
193
}
194
```
195
196
## Test Execution Flow
197
198
### Task-Based Execution
199
200
```scala { .api }
201
trait Task {
202
def taskDef(): TaskDef
203
def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task]
204
}
205
206
trait TaskDef {
207
def fullyQualifiedName(): String
208
def fingerprint(): Fingerprint
209
def explicitlySpecified(): Boolean
210
def selectors(): Array[Selector]
211
}
212
```
213
214
**Example Test Execution:**
215
```scala
216
class JUnitTask(taskDef: TaskDef, settings: RunSettings) extends Task {
217
def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task] = {
218
val reporter = new Reporter(eventHandler, loggers, settings, taskDef)
219
220
try {
221
reporter.reportRunStarted()
222
223
val testClass = Class.forName(taskDef.fullyQualifiedName())
224
val testInstance = testClass.newInstance()
225
226
// Execute @Before methods
227
executeBeforeMethods(testInstance)
228
229
// Execute test methods
230
val testMethods = findTestMethods(testClass)
231
testMethods.foreach(executeTestMethod(testInstance, _, reporter))
232
233
// Execute @After methods
234
executeAfterMethods(testInstance)
235
236
reporter.reportRunFinished(failedCount, ignoredCount, totalCount, duration)
237
238
} catch {
239
case e: Exception =>
240
reporter.reportErrors("Test execution failed", None, 0.0, List(e))
241
}
242
243
Array.empty // No subtasks
244
}
245
}
246
```
247
248
## Event Reporting
249
250
### JUnitEvent
251
252
```scala { .api }
253
class JUnitEvent(
254
taskDef: TaskDef,
255
status: Status,
256
selector: Selector,
257
throwable: OptionalThrowable = new OptionalThrowable,
258
duration: Long = -1L
259
) extends Event {
260
def fullyQualifiedName(): String = taskDef.fullyQualifiedName()
261
def fingerprint(): Fingerprint = taskDef.fingerprint()
262
}
263
```
264
265
**Event Status Types:**
266
- `Status.Success` - Test passed
267
- `Status.Failure` - Test failed with assertion error
268
- `Status.Error` - Test failed with unexpected exception
269
- `Status.Skipped` - Test was ignored or assumption failed
270
271
### Reporter Integration
272
273
```scala { .api }
274
class Reporter(
275
eventHandler: EventHandler,
276
loggers: Array[Logger],
277
settings: RunSettings,
278
taskDef: TaskDef
279
) {
280
def reportTestStarted(method: String): Unit
281
def reportTestFinished(method: String, succeeded: Boolean, timeInSeconds: Double): Unit
282
def reportErrors(prefix: String, method: Option[String], timeInSeconds: Double, errors: List[Throwable]): Unit
283
def reportIgnored(method: Option[String]): Unit
284
}
285
```
286
287
## Custom Runner Implementation
288
289
```scala
290
class CustomParameterizedRunner(testClass: Class[_]) extends Runner {
291
private val parameters = getParameters(testClass)
292
293
def done(): String = ""
294
295
def tasks(taskDefs: Array[TaskDef]): Array[Task] = {
296
taskDefs.flatMap { taskDef =>
297
parameters.map { param =>
298
new ParameterizedTask(taskDef, param)
299
}
300
}
301
}
302
303
private def getParameters(clazz: Class[_]): Array[Any] = {
304
// Extract parameters from @Parameters annotation or method
305
Array(1, 2, 3, 4, 5) // Example parameters
306
}
307
}
308
309
class ParameterizedTask(taskDef: TaskDef, parameter: Any) extends Task {
310
def taskDef(): TaskDef = taskDef
311
312
def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task] = {
313
// Execute test with parameter
314
val testClass = Class.forName(taskDef.fullyQualifiedName())
315
val constructor = testClass.getConstructor(parameter.getClass)
316
val testInstance = constructor.newInstance(parameter)
317
318
// Execute test methods on parameterized instance
319
executeTestMethods(testInstance, eventHandler, loggers)
320
321
Array.empty
322
}
323
}
324
```
325
326
## Integration with Build Tools
327
328
### SBT Integration
329
330
```scala
331
// build.sbt
332
libraryDependencies += "org.scala-js" %%% "scalajs-junit-test-runtime" % "1.19.0" % Test
333
334
// Enable JUnit testing
335
testFrameworks += new TestFramework("org.scalajs.junit.JUnitFramework")
336
```
337
338
### Test Configuration
339
340
```scala
341
// Configure test execution
342
Test / testOptions += Tests.Argument(
343
TestFrameworks.JUnit,
344
"-v", // Verbose
345
"-s", // Decode Scala names
346
"-a" // Log assertions
347
)
348
```
349
350
## Advanced Runner Features
351
352
### Parallel Execution Support
353
354
```scala
355
class ParallelJUnitRunner extends Runner {
356
def tasks(taskDefs: Array[TaskDef]): Array[Task] = {
357
taskDefs.map(new ParallelTask(_))
358
}
359
}
360
361
class ParallelTask(taskDef: TaskDef) extends Task {
362
def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task] = {
363
// Execute test in parallel-safe manner
364
Future {
365
executeTestSafely(eventHandler, loggers)
366
}
367
Array.empty
368
}
369
}
370
```
371
372
### Test Filtering
373
374
```scala
375
class FilteringRunner(filter: String => Boolean) extends Runner {
376
def tasks(taskDefs: Array[TaskDef]): Array[Task] = {
377
taskDefs
378
.filter(taskDef => filter(taskDef.fullyQualifiedName()))
379
.map(new FilteredTask(_))
380
}
381
}
382
```
383
384
The runner framework provides flexibility for custom test execution strategies while maintaining compatibility with standard JUnit test discovery and reporting mechanisms.