0
# Transforms and Extensions
1
2
Extensible transform system for customizing test and suite behavior. Transforms allow you to modify individual tests, entire suites, and return value handling without changing the core test definitions.
3
4
## Capabilities
5
6
### TestTransforms - Individual Test Transformations
7
8
Transform individual tests to modify their behavior, add metadata, or implement custom logic.
9
10
```scala { .api }
11
/**
12
* Trait providing test transformation capabilities (mixed into BaseFunSuite)
13
*/
14
trait TestTransforms {
15
/**
16
* Get the list of test transforms to apply
17
* Override this method to customize test transformation
18
*/
19
def munitTestTransforms: List[TestTransform]
20
21
/**
22
* Apply test transforms to a single test
23
* This method is called automatically for each test
24
*/
25
def munitTestTransform(test: Test): Test
26
27
/** Check if flaky tests should be treated as passing */
28
def munitFlakyOK: Boolean
29
}
30
31
/**
32
* A transformation that can be applied to individual tests
33
* @param name Descriptive name for the transform
34
* @param fn Function that transforms a Test into a modified Test
35
*/
36
final class TestTransform(val name: String, fn: Test => Test) extends Function1[Test, Test] {
37
def apply(test: Test): Test = fn(test)
38
}
39
```
40
41
**Built-in Test Transforms:**
42
43
```scala { .api }
44
/** Transform that handles tests marked with the Fail tag */
45
def munitFailTransform: TestTransform
46
47
/** Transform that handles tests marked with the Flaky tag */
48
def munitFlakyTransform: TestTransform
49
50
/**
51
* Transform that appends additional context to failure messages
52
* @param buildSuffix Function to generate additional context for a test
53
*/
54
def munitAppendToFailureMessage(buildSuffix: Test => Option[String]): TestTransform
55
```
56
57
**Usage Examples:**
58
59
```scala
60
class TransformExamples extends FunSuite {
61
// Custom transform to add timing information
62
val timingTransform = new TestTransform(
63
"timing",
64
test => test.withBodyMap { originalBody =>
65
val start = System.currentTimeMillis()
66
originalBody.andThen { _ =>
67
val duration = System.currentTimeMillis() - start
68
println(s"Test '${test.name}' took ${duration}ms")
69
Future.unit
70
}
71
}
72
)
73
74
// Custom transform to retry flaky tests
75
val retryTransform = new TestTransform(
76
"retry",
77
test => {
78
if (test.tags.contains(new Tag("retry"))) {
79
test.withBodyMap { originalBody =>
80
originalBody.recoverWith {
81
case _: AssertionError =>
82
println(s"Retrying test: ${test.name}")
83
originalBody
84
}
85
}
86
} else {
87
test
88
}
89
}
90
)
91
92
override def munitTestTransforms: List[TestTransform] =
93
super.munitTestTransforms ++ List(timingTransform, retryTransform)
94
95
test("normal test") {
96
assertEquals(1 + 1, 2)
97
}
98
99
test("flaky network test".tag(new Tag("retry"))) {
100
// This test will be retried once if it fails
101
callExternalAPI()
102
}
103
}
104
```
105
106
### SuiteTransforms - Suite-Level Transformations
107
108
Transform entire test suites to filter, reorder, or modify collections of tests.
109
110
```scala { .api }
111
/**
112
* Trait providing suite transformation capabilities (mixed into BaseFunSuite)
113
*/
114
trait SuiteTransforms {
115
/**
116
* Get the list of suite transforms to apply
117
* Override this method to customize suite transformation
118
*/
119
def munitSuiteTransforms: List[SuiteTransform]
120
121
/**
122
* Apply suite transforms to the entire test list
123
* This method is called automatically when collecting tests
124
*/
125
def munitSuiteTransform(tests: List[Test]): List[Test]
126
127
/** Override to ignore the entire test suite */
128
def munitIgnore: Boolean = false
129
130
/** Check if running in continuous integration environment */
131
def isCI: Boolean
132
}
133
134
/**
135
* A transformation that can be applied to entire test suites
136
* @param name Descriptive name for the transform
137
* @param fn Function that transforms a List[Test] into a modified List[Test]
138
*/
139
final class SuiteTransform(val name: String, fn: List[Test] => List[Test]) extends Function1[List[Test], List[Test]] {
140
def apply(tests: List[Test]): List[Test] = fn(tests)
141
}
142
```
143
144
**Built-in Suite Transforms:**
145
146
```scala { .api }
147
/** Transform that handles ignored test suites */
148
def munitIgnoreSuiteTransform: SuiteTransform
149
150
/** Transform that handles the Only tag (runs only marked tests) */
151
def munitOnlySuiteTransform: SuiteTransform
152
```
153
154
**Usage Examples:**
155
156
```scala
157
class SuiteTransformExamples extends FunSuite {
158
// Custom transform to randomize test order
159
val randomizeTransform = new SuiteTransform(
160
"randomize",
161
tests => scala.util.Random.shuffle(tests)
162
)
163
164
// Custom transform to group tests by tags
165
val groupByTagTransform = new SuiteTransform(
166
"groupByTag",
167
tests => {
168
val (slowTests, fastTests) = tests.partition(_.tags.contains(Slow))
169
fastTests ++ slowTests // Run fast tests first
170
}
171
)
172
173
// Custom transform to skip integration tests in CI
174
val skipIntegrationInCI = new SuiteTransform(
175
"skipIntegrationInCI",
176
tests => {
177
if (isCI) {
178
tests.filterNot(_.tags.contains(new Tag("integration")))
179
} else {
180
tests
181
}
182
}
183
)
184
185
override def munitSuiteTransforms: List[SuiteTransform] =
186
super.munitSuiteTransforms ++ List(
187
groupByTagTransform,
188
skipIntegrationInCI
189
)
190
191
test("fast unit test") {
192
assertEquals(1 + 1, 2)
193
}
194
195
test("slow integration test".tag(Slow).tag(new Tag("integration"))) {
196
// This test may be skipped in CI
197
performIntegrationTest()
198
}
199
}
200
```
201
202
### ValueTransforms - Return Value Transformations
203
204
Transform test return values to handle different types of results (Future, Try, etc.) and convert them to the standard `Future[Any]` format.
205
206
```scala { .api }
207
/**
208
* Trait providing value transformation capabilities (mixed into BaseFunSuite)
209
*/
210
trait ValueTransforms {
211
/**
212
* Get the list of value transforms to apply
213
* Override this method to customize value transformation
214
*/
215
def munitValueTransforms: List[ValueTransform]
216
217
/**
218
* Apply value transforms to convert any test return value to Future[Any]
219
* This method is called automatically for each test body
220
*/
221
def munitValueTransform(testValue: => Any): Future[Any]
222
}
223
224
/**
225
* A transformation that can be applied to test return values
226
* @param name Descriptive name for the transform
227
* @param fn Partial function that transforms specific types to Future[Any]
228
*/
229
final class ValueTransform(val name: String, fn: PartialFunction[Any, Future[Any]]) extends Function1[Any, Option[Future[Any]]] {
230
def apply(value: Any): Option[Future[Any]] = fn.lift(value)
231
}
232
```
233
234
**Built-in Value Transforms:**
235
236
```scala { .api }
237
/** Transform that handles Future return values */
238
def munitFutureTransform: ValueTransform
239
```
240
241
**Usage Examples:**
242
243
```scala
244
import scala.util.{Try, Success, Failure}
245
import scala.concurrent.Future
246
247
class ValueTransformExamples extends FunSuite {
248
// Custom transform to handle Try values
249
val tryTransform = new ValueTransform(
250
"try",
251
{
252
case Success(value) => Future.successful(value)
253
case Failure(exception) => Future.failed(exception)
254
}
255
)
256
257
// Custom transform to handle custom Result type
258
case class Result[T](value: T, errors: List[String]) {
259
def isSuccess: Boolean = errors.isEmpty
260
}
261
262
val resultTransform = new ValueTransform(
263
"result",
264
{
265
case result: Result[_] =>
266
if (result.isSuccess) {
267
Future.successful(result.value)
268
} else {
269
Future.failed(new AssertionError(s"Result failed: ${result.errors.mkString(", ")}"))
270
}
271
}
272
)
273
274
override def munitValueTransforms: List[ValueTransform] =
275
super.munitValueTransforms ++ List(tryTransform, resultTransform)
276
277
test("test returning Try") {
278
Try {
279
assertEquals(2 + 2, 4)
280
"success"
281
}
282
}
283
284
test("test returning custom Result") {
285
val data = processData()
286
Result(data, List.empty) // Success case
287
}
288
289
test("test returning failed Result") {
290
Result("", List("validation error")) // This will fail the test
291
}
292
}
293
```
294
295
## Advanced Transform Patterns
296
297
### Conditional Transforms
298
299
```scala
300
class ConditionalTransforms extends FunSuite {
301
// Transform that only applies to tests with specific tags
302
val debugTransform = new TestTransform(
303
"debug",
304
test => {
305
if (test.tags.contains(new Tag("debug"))) {
306
test.withBodyMap { originalBody =>
307
println(s"[DEBUG] Starting test: ${test.name}")
308
originalBody.andThen { result =>
309
println(s"[DEBUG] Completed test: ${test.name}")
310
result
311
}
312
}
313
} else {
314
test
315
}
316
}
317
)
318
319
// Transform that modifies behavior based on environment
320
val environmentTransform = new SuiteTransform(
321
"environment",
322
tests => {
323
val environment = System.getProperty("test.environment", "local")
324
environment match {
325
case "ci" => tests.filterNot(_.tags.contains(new Tag("local-only")))
326
case "staging" => tests.filterNot(_.tags.contains(new Tag("prod-only")))
327
case _ => tests
328
}
329
}
330
)
331
}
332
```
333
334
### Metrics and Reporting
335
336
```scala
337
class MetricsTransforms extends FunSuite {
338
private val testMetrics = scala.collection.mutable.Map[String, Long]()
339
340
val metricsTransform = new TestTransform(
341
"metrics",
342
test => test.withBodyMap { originalBody =>
343
val start = System.nanoTime()
344
originalBody.andThen { result =>
345
val duration = System.nanoTime() - start
346
testMetrics(test.name) = duration
347
result
348
}
349
}
350
)
351
352
override def munitTestTransforms = List(metricsTransform)
353
354
override def afterAll(): Unit = {
355
super.afterAll()
356
println("Test Execution Times:")
357
testMetrics.toList.sortBy(_._2).foreach { case (name, nanos) =>
358
println(f" $name: ${nanos / 1000000.0}%.2f ms")
359
}
360
}
361
}
362
```
363
364
### Error Context Enhancement
365
366
```scala
367
class ErrorContextTransforms extends FunSuite {
368
val contextTransform = new TestTransform(
369
"context",
370
test => test.withBodyMap { originalBody =>
371
originalBody.recoverWith {
372
case e: AssertionError =>
373
val enhancedMessage = s"${e.getMessage}\n" +
374
s"Test: ${test.name}\n" +
375
s"Tags: ${test.tags.map(_.value).mkString(", ")}\n" +
376
s"Location: ${test.location}"
377
Future.failed(new AssertionError(enhancedMessage, e))
378
}
379
}
380
)
381
382
override def munitTestTransforms = List(contextTransform)
383
}
384
```
385
386
## Transform Configuration
387
388
### Environment-Based Configuration
389
390
```scala
391
class EnvironmentBasedSuite extends FunSuite {
392
override def munitFlakyOK: Boolean = {
393
// Allow flaky tests in development but not in CI
394
!isCI && System.getProperty("munit.flaky.ok", "false").toBoolean
395
}
396
397
override def isCI: Boolean = {
398
System.getenv("CI") != null ||
399
System.getProperty("CI") != null
400
}
401
}
402
```
403
404
### Custom Transform Ordering
405
406
The order of transforms matters - they are applied in sequence:
407
408
```scala
409
override def munitTestTransforms: List[TestTransform] = List(
410
timingTransform, // Applied first
411
retryTransform, // Applied second
412
loggingTransform // Applied last
413
) ++ super.munitTestTransforms // Include built-in transforms
414
```