Testing with MUnit
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Scala 3.10.0 (released 2026-10-12, the current Scala Next line) with Scala CLI 1.17.1 on JDK 26 and the Scala Toolkit 0.9.2, whose test module brings MUnit 1.2.1. Every example whose output is shown was run on that setup on 2026-10-16, and the trail project of the "Writing a test suite" section was also run on Scala 3.9.0 (the 3.9 LTS line) and on Scala 3.3.8 (the 3.3 LTS line) with the same results; commands that are not run are labelled "not run here" with the reason. Nothing on this page needs a 3.10-only feature, and MUnit itself is an ordinary library that works the same on every Scala 3 release and on Scala 2.13 (the Scala 2 form of each snippet is shown where it differs). The Scala compilers need JDK 17 or later.
MUnit is the test library of the official Scala Toolkit. Its design goal is a single, quickly learned style: a class extends munit.FunSuite,
calls test("name") { ... } for each test, and uses a handful of assertion methods. Failures print the source location and, for assertEquals, a diff
of the two values. For the Java counterpart see Java Testing (JUnit 5 and Mockito) and, on the
other JVM language of this guide, Groovy: Testing with JUnit & groovy-test.
This page covers the basics; asynchronous tests, fixtures, flaky tests and the other frameworks are on
MUnit Async, Fixtures & Other Frameworks, and generated inputs are on
Property-Based Testing.
What the Scala Toolkit is
The Scala Toolkit is a set of libraries, chosen and kept compatible by the Scala team, for the tasks that short programs perform again and again: working with files and processes, parsing JSON, sending HTTP requests, and unit testing. It supports Scala 3 and Scala 2 on the JVM, Scala.js and Scala Native. The pieces and the pages of this guide that cover them are:
| Task | Library | Page |
|---|---|---|
Unit testing |
MUnit |
This page |
Files and processes |
OS-Lib |
|
JSON |
uPickle |
|
HTTP requests |
sttp |
|
Web servers |
Cask |
The Toolkit is published as two artifacts, and the split matters because test libraries must not leak into production code:
-
org.scala-lang::toolkitholds OS-Lib, uPickle and sttp. Version 0.9.2 is the one used here. -
org.scala-lang::toolkit-testadds MUnit to that set. For 0.9.2 it brings MUnit 1.2.1 (checked in the resolved class path of this page’s examples). Cask is not part of either artifact and is added as a separate dependency, as the Cask page shows.
The version number is still "0.x", and each release pins versions of the libraries that were tested together. The Scala 3 Book and the Toolkit tutorials that this page derives from were written for Toolkit 0.7.0, MUnit 1.1.0 and Scala 3.4; at the time of this check the current Toolkit is 0.9.2.
Getting MUnit
A test needs the MUnit classes on its class path, but only in the test scope. Each build tool has its own way to say it.
Scala CLI
The directive for the Toolkit’s test module is test.toolkit. A directive that starts with test. applies only to test sources.
The test sources are the files whose name ends in .test.scala, and every file inside a directory called test.
//> using scala 3.10.0
//> using test.toolkit 0.9.2
Two variations are worth knowing:
-
//> using toolkit 0.9.2(withouttest.) puts the whole Toolkit, MUnit included, in the main scope. That is what a script that uses OS-Lib and uPickle wants, and it is wrong for a library, whose main code should not depend on a test library. When this page’s project (which usestest.toolkit) is exported to sbt, the tool writes bothtoolkitandtoolkit-testwith theTestconfiguration (checked withscala --power export --sbt). -
//> using test.toolkit defaultasks for the Toolkit version that this Scala CLI release considers the default. On Scala CLI 1.17.1 that was 0.9.2 with MUnit 1.2.1, as the resolved class path showed. The Scala 3 Book also showslatest; on the version used here it still works but prints a deprecation warning that tells you to usedefault, so write an explicit number when you want a build that does not change.
[warn] ./T.test.scala:2:11
[warn] Using 'latest' for toolkit is deprecated, use 'default' to get more stable behaviour
[warn] //> using test.toolkit latest
[warn] ^^^^^^^^^^^
Without the Toolkit, MUnit can be added by itself. Its latest release when this page was written is 1.3.6, which is newer than the one in the Toolkit:
//> using test.dep org.scalameta::munit:1.3.6
Source: MUnit: Quick start
Because the Toolkit pins MUnit to 1.2.1, a feature added after that release, such as assertMatches, is not there until you move to the
standalone dependency (the assertions section below shows the exact compiler message). Scala CLI also tells you when a pinned MUnit is
behind: with org.scalameta::munit:1.2.1 it prints "munit is outdated, update to 1.3.6" as a hint.
The scope is enforced by the compiler. A class that extends munit.FunSuite in an ordinary source file does not compile when MUnit is a test-only dependency:
-- [E006] Not Found Error: ./Bad.scala:1:18
1 |class Bad extends munit.FunSuite
| ^^^^^
| Not found: munit - did you mean Unit?
sbt
sbt takes the Toolkit’s test module as a Test dependency, so the main code never sees it. sbt 2.0.x (2.0.10 when this was checked) and sbt 1.x use the same line:
lazy val example = project
.in(file("."))
.settings(
scalaVersion := "3.10.0",
libraryDependencies += "org.scala-lang" %% "toolkit-test" % "0.9.2" % Test
)
Or, for MUnit alone: libraryDependencies += "org.scalameta" %% "munit" % "1.3.6" % Test. Test sources go in src/test/scala.
sbt is not installed on the machine used for this page, so the sbt and Mill snippets are not run here; they were compared with the Scala CLI export of the same
project (which writes the same org.scala-lang %% toolkit-test % 0.9.2 % Test coordinates) and with the Toolkit tutorial. See
sbt & Build Tools for the build itself.
Mill
In Mill the test module is a nested object that extends TestModule.Munit:
object example extends ScalaModule {
def scalaVersion = "3.10.0"
object test extends ScalaTests with TestModule.Munit {
def ivyDeps = Agg(ivy"org.scala-lang::toolkit-test:0.9.2")
}
}
Mill’s API has changed between releases (the Toolkit tutorial uses Tests; recent Mill versions call the class ScalaTests and the dependency
method mvnDeps), so treat this block as a sketch and check it against the Mill version in use. Test sources go in test/src. Not run here: Mill is not installed.
Writing a test suite
A group of tests in one class is a test suite. By convention there is one suite per class or per feature under test. A suite is a class that extends
munit.FunSuite; each test is a call to test, whose second argument is the body. A test passes when its body finishes without throwing, and fails
when an assertion throws.
The scenario for this page is the trail planner used elsewhere in this section. Here is the code under test:
//> using scala 3.10.0
//> using test.toolkit 0.9.2
package trail
case class Hike(name: String, km: Double, climb: Int)
object Hike:
def pace(h: Hike, hours: Double): Double =
if hours <= 0 then throw IllegalArgumentException(s"hours must be positive: $hours")
h.km / hours
def totalKm(hikes: List[Hike]): Double = hikes.map(_.km).sum
and its test file, next to it in the same directory and named TrailTests.test.scala:
package trail
class TrailTests extends munit.FunSuite:
val hikes = List(Hike("Ridge", 8.5, 620), Hike("Lake", 4.0, 120))
test("total distance") {
val obtained = totalKm(hikes)
val expected = 12.5
assertEquals(obtained, expected)
}
test("every hike is short enough for a day") {
assert(clue(hikes.map(_.km)).forall(_ < 20))
}
test("pace rejects zero hours") {
val e = intercept[IllegalArgumentException](Hike.pace(hikes.head, 0))
assert(clue(e.getMessage).contains("positive"))
}
scala test .
trail.TrailTests:
+ total distance 0.006s
+ every hike is short enough for a day 0.001s
+ pace rejects zero hours 0.001s
The + in front of a name means the test passed. The directive lines belong to the whole Scala CLI project, so they appear once, in the main file; the test file does not repeat them.
The same project, with only //> using scala changed, printed the same report on 3.9.0 and on 3.3.8.
Three details of the style are worth noting:
-
The braces after
test("...")are the argument, not a block that could be dropped.testtakes the name and the body as two arguments (the body is by-name). The braceless form from Optional Braces & Indentation applies to the class (class TrailTests extends munit.FunSuite:). The braces keep the code identical in Scala 2 and Scala 3, which is why the official tutorial and this page use them. -
In Scala 2 the class has braces, and it extends the same trait:
class TrailTests extends munit.FunSuite { ... }. Nothing else changes in the body. -
The suite’s constructor runs once, and
testcalls only register tests. Avalin the class body, such ashikesabove, is initialised once for the whole suite. MUnit runs the tests of a suite sequentially, in the order they were declared. Fixtures that create fresh state for each test are the subject of the next page.
Scala CLI finds the tests by file name: a file ending in .test.scala, or any file in a directory named test, is a test source. The layouts
for the three build tools are:
| Tool | Layout |
|---|---|
Scala CLI |
|
sbt |
|
Mill |
|
One pitfall appears when the code under test and the tests share a file with a package clause: Scala 3 warns (error code E222) when
top-level definitions in a file whose name contains a dot are put in a package, because the generated name Tags.test$package is not safe on the class path.
Put shared values inside an object, or in a file whose name is a plain identifier, as the Tags object in the tagging section below does.
Assertions
Every test needs at least one assertion, because a test with none passes whatever the code does. The methods that munit.FunSuite inherits from
munit.Assertions are listed here with the version in the Toolkit’s MUnit 1.2.1.
| Method | What it checks |
|---|---|
|
The boolean condition is true. Wrap values in |
|
The two values are equal by |
|
The two values are different. |
|
Two doubles differ by at most |
|
Two multi-line strings are equal after ignoring trailing whitespace, line-ending style and ANSI colour codes. |
|
The body throws |
|
Fails the test at once. |
|
Returns the compiler’s error text for a snippet (an empty string when it compiles). The argument must be a string literal. |
|
Skips the test when the condition is false instead of failing it. |
The order of the arguments is the opposite of JUnit’s. MUnit puts the obtained value first and the expected one second, assertEquals(obtained, expected), while JUnit 5 has
assertEquals(expected, actual). Swapping them does not change whether a test passes; it only inverts which side the failure report calls expected. The diff
header that MUnit 1.2.1 prints is Diff (- expected, + obtained), so a line starting with - is what the test wanted and a line starting with + is what the code produced.
(The Toolkit tutorial shows the older header - obtained, + expected; the output below is from the current library.)
assertEquals and its diff
This suite deliberately fails, to show what each report looks like. Each report below is the output of one of these tests, with the stack-trace lines omitted:
//> using scala 3.10.0
//> using test.toolkit 0.9.2
package trail
case class Hike(name: String, km: Double, climb: Int)
class FailureShowcase extends munit.FunSuite:
test("int mismatch") {
val obtained = 2 + 3
val expected = 4
assertEquals(obtained, expected)
}
test("case class diff") {
assertEquals(Hike("Ridge", 8.5, 620), Hike("Ridge", 8.5, 650))
}
==> X trail.FailureShowcase.int mismatch 0.029s munit.ComparisonFailException: Fails.test.scala:13
12: val expected = 4
13: assertEquals(obtained, expected)
14: }
values are not the same
=> Obtained
5
=> Diff (- expected, + obtained)
-4
+5
==> X trail.FailureShowcase.case class diff 0.003s munit.ComparisonFailException: Fails.test.scala:17
16: test("case class diff") {
17: assertEquals(Hike("Ridge", 8.5, 620), Hike("Ridge", 8.5, 650))
18: }
values are not the same
=> Obtained
Hike(
name = "Ridge",
km = 8.5,
climb = 620
)
=> Diff (- expected, + obtained)
km = 8.5,
- climb = 650
+ climb = 620
)
Source: MUnit: Assertions, assertEquals
The report starts with ==> X, the suite and test name, the exception class and the line in your test file. Then come the source lines around the
failing assertion, the obtained value pretty-printed over several lines (case classes print with their field names, because of the printer MUnit uses), and a unified diff with one line of context. For large nested values only the differing lines are shown, which is the point of the library. The exception type is
munit.ComparisonFailException, a subclass of munit.FailException.
Floating point needs a tolerance, and the plain assertEquals shows why:
test("doubles") {
assertEqualsDouble(0.1 + 0.2, 0.3, 0.0001)
assertEquals(0.1 + 0.2, 0.3)
}
==> X trail.FailureShowcase.doubles 0.004s munit.ComparisonFailException: Fails.test.scala:50
49: assertEqualsDouble(0.1 + 0.2, 0.3, 0.0001)
50: assertEquals(0.1 + 0.2, 0.3)
51: }
values are not the same
=> Obtained
0.30000000000000004
=> Diff (- expected, + obtained)
-0.3
+0.30000000000000004
Source: MUnit: Assertions, assertEquals
The first line passed because of the delta; the second failed because 0.1 + 0.2 == 0.3 is false in binary floating point (the
same in Java and Python). Only the first failing assertion of a test is reported: an assertion throws, so the later ones in the same test body do not run. In a test
with assertEquals(1, 2) followed by assertEquals(3, 4), the report mentions only 1 against 2. Split such a test in two when you want both reported.
assert and clue
assert(cond) fails with the message assertion failed, which says nothing about the values. clue(expr) records the text of the expression and its value, and
the report prints a Clues block:
test("boolean with clues") {
val km = List(8.5, 4.0, 21.0)
val limit = 20.0
assert(clue(km).forall(_ < clue(limit)))
}
==> X trail.FailureShowcase.boolean with clues 0.114s munit.FailException: Fails.test.scala:23 assertion failed
22: val limit = 20.0
23: assert(clue(km).forall(_ < clue(limit)))
24: }
Clues {
km: List[Double] = List(
8.5,
4.0,
21.0
)
limit: Double = 20.0
limit: Double = 20.0
limit: Double = 20.0
}
Source: MUnit: Assertions, assert
The clue for limit appears three times because the lambda it is inside ran once per element. A clue records every evaluation, so put it outside loops and
lambdas when the output matters. The Scala 3 compiler records the source text of each clue on its own; the MUnit documentation notes that Scala 2 needs the
compiler option -Yrangepos for clue to show the expression text (without it the names are empty).
assertNotEquals reports through the same exception:
==> X trail.FailureShowcase.not equals 0.004s munit.ComparisonFailException: Fails.test.scala:27 values are the same, expected 2 different values: 2 is equal to 2
26: test("not equals") {
27: assertNotEquals(1 + 1, 2)
28: }
assertNoDiff for text
When comparing generated text, a stray trailing space or a Windows line ending makes assertEquals unhelpful. assertNoDiff compares as the eye does, and its report shows only
the differing lines:
test("no diff") {
assertNoDiff("val x = 41\nval y = 43", "val x = 41\nval y = 42")
}
==> X trail.FailureShowcase.no diff 0.006s munit.ComparisonFailException: Fails.test.scala:31
30: test("no diff") {
31: assertNoDiff("val x = 41\nval y = 43", "val x = 41\nval y = 42")
32: }
diff assertion failed
=> Obtained
val x = 41
val y = 43
=> Diff (- expected, + obtained)
val x = 41
-val y = 42
+val y = 43
Source: MUnit: Assertions, assertNoDiff
fail
fail(message, clues(...)) is for branches that must not be reached, such as the case _ of a match that should have been exhaustive:
test("fail with clue") {
val a = 1
val b = 2
fail("explicit failure", clues(a + b))
}
==> X trail.FailureShowcase.fail with clue 0.0s munit.FailException: Fails.test.scala:45 explicit failure
44: val b = 2
45: fail("explicit failure", clues(a + b))
46: }
Clues {
a + b: Int = 3
}
Source: MUnit: Assertions, fail
Comparing values of different types
assertEquals is typed: it refuses to compare two values whose types are unrelated, because that comparison is almost always a mistake (an Option[Int] can never equal an Int).
The refusal is a compile error with a long but useful message; this is the real text for assertEquals(1, "one"):
-- [E172] Type Error: ./Types.test.scala:6:26
6 | assertEquals(1, "one")
| ^
| Can't compare these two types:
| First type: Int
| Second type: String
| Possible ways to fix this error:
| Alternative 1: provide an implicit instance for Compare[Int, String]
| Alternative 2: upcast either type into `Any` or a shared supertype.
| I found:
|
| munit.Compare.compareSubtypeWithSupertype[Int, String](
| /* missing */summon[Int <:< String])
|
| But no implicit values were found that match type Int <:< String.
The comparison works when one type is a subtype of the other, so assertEquals(xs: Seq[Int], List(1, 2)) passes. The two fixes the message names are:
-
Say what you mean. Upcast with
assertEquals[Any, Any](1, "one")(it then compiles and, as it should, fails: the report shows-oneand+1). -
Teach MUnit a comparison with a
givenofmunit.Compare[A, B]. This compiles and passes (checked):
test("custom Compare") {
given munit.Compare[Int, String] with
def isEqual(obtained: Int, expected: String): Boolean = obtained.toString == expected
assertEquals(42, "42")
}
Source: MUnit: Assertions, assertEquals
In Scala 2 the instance is an implicit val or implicit object; the error text mentions an "implicit instance" for that reason. See
Givens & Using Clauses for the mechanism.
compileErrors
compileErrors turns "this must not compile" into a test, which is how you check that a type-level guarantee holds. It returns the compiler message as a string, so it combines with
assertNoDiff. The argument must be a string literal known at compile time (not a variable or an interpolated string). This test passes on Scala 3.10.0; the text is the
exact message of that compiler, so it is brittle across versions, and a test that merely needs to know that the code is rejected can instead
assert that the result is non-empty:
test("compile errors") {
assertNoDiff(
compileErrors("val x: String = 2"),
"""|error:
|Found: (2 : Int)
|Required: String
|val x: String = 2
| ^""".stripMargin
)
}
test("assertions that compile are not errors") {
assertEquals(compileErrors("val y: Int = 2"), "")
}
Run in the same project as the earlier test, the report was:
TypesSuite:
+ subtype is fine 0.021s
+ compile errors 0.007s
+ assertions that compile are not errors 0.0s
Source: MUnit: Assertions, compileErrors
A first attempt at the expected text, copied from the MUnit documentation (a Scala 2.13 message with type mismatch; found : Int(2)), failed with the usual diff;
the Scala 3 wording of the same error is the one above. The same applies to your own tests: copy the obtained text from a first failing run.
assertMatches
Later MUnit releases add assertMatches(value) { case pattern => predicate }. It is not in MUnit 1.2.1, which is what the Toolkit 0.9.2 brings:
-- [E006] Not Found Error: ./Fails.test.scala:32:4
32 | assertMatches(results) { case List(Left(_), Right(n)) => n % 2 == 1 }
| ^^^^^^^^^^^^^
| Not found: assertMatches
With //> using test.dep org.scalameta::munit:1.3.6 the same call compiles, and a pattern that does not apply fails with the message
assertion failed: predicate not defined for value: List(Left(parse error), Right(42)). If you need it, move to the standalone dependency, or use an ordinary
match with fail in the other case. Note also that with MUnit 1.3.6 Scala CLI printed the report in the JUnit style (Test run MatchSuite started, Test run MatchSuite finished: 1 failed, 0 ignored, 2 total)
rather than the +/X lines of 1.2.1; both come from the same library, and the content of a failure (source lines, obtained value, diff) is the same.
Testing exceptions
intercept[E] { body } passes when the body throws an exception of type E (or a subclass) and returns the exception, so you can assert on its message or cause.
The Toolkit tutorial uses OS-Lib to read a missing file; this runs as written:
import java.nio.file.NoSuchFileException
class MiscSuite extends munit.FunSuite:
test("read missing file") {
val missingFile = os.pwd / "missing.txt"
val exception = intercept[NoSuchFileException](os.read(missingFile))
assert(clue(exception.getMessage).contains("missing.txt"))
}
MiscSuite:
+ read missing file 0.019s
Two things make intercept safer than a hand-written try/catch: it fails when nothing is thrown, and it fails when something other than E is thrown. The two reports:
==> X trail.FailureShowcase.intercept without exception 0.002s munit.FailException: Fails.test.scala:35 expected exception of type 'java.lang.IllegalArgumentException' but body evaluated successfully
34: test("intercept without exception") {
35: intercept[IllegalArgumentException](1 + 1)
36: }
interceptMessage[E]("text")(body) checks the type and the exact message in one assertion. When the message differs it reports the actual one:
==> X trail.FailureShowcase.intercept message 0.001s munit.FailException: intercept failed, exception 'java.lang.IllegalArgumentException' had message 'boom', which was different from expected message 'other'
Caused by: java.lang.IllegalArgumentException: boom
intercept is for exceptions that are the specified behaviour of the code. When the failure is better modelled as a value, as Either and Try do, the test just compares values with assertEquals
and no exception handling is involved: see Either & Try and
Exceptions, boundary & Early Exit. JUnit spells the same idea assertThrows.
Running tests
All the tests
scala test .
scala test compiles the project and its test sources, and runs every test framework it finds on the test class path. The report is the list of + lines shown above. On a failure, the failure
reports are printed, the process ends with exit status 1, and the last line is Error: munit tests failed. (the exit status was checked), so a CI job
that runs scala test . needs no extra scripting. The first run downloads the Toolkit and its dependencies from Maven Central through Coursier;
later runs use the cache.
The report of a failing run is the one in the previous sections, with a ==> X line per failure. In sbt, sbt test prints the same lines with a summary such as Passed: Total 1, Failed 0, Errors 0, Passed 1,
and in Mill the task is ./mill example.test.test (both not run here).
One suite, or tests selected by name
Scala CLI selects test classes with --test-only, which takes a fully qualified class name or a glob, and the framework itself
accepts a name glob after a --. A suite with these tests is used for the examples (it is the one shown in the tagging section below):
scala test . --test-only trail.RouteSuite
scala test . --test-only 'trail.O*'
scala test . -- '*elevation*'
scala test . -- '*RouteSuite.untagged'
With the second command only trail.OtherSuite was run; with the third only the test elevation profile of any suite whose full name contains elevation:
trail.RouteSuite:
+ elevation profile 0.01s
scala test . -- '*RouteSuite.untagged' selected the test called untagged in RouteSuite only. The glob is matched against the suite’s fully qualified name, a dot, and the test name.
MUnit also has --tests=REGEXP, a regular expression matched against the whole name. That is a source of confusion: -- --tests=elevation selects nothing (everything is reported as ignored),
because the regex has to match the full trail.RouteSuite.elevation profile. With .* on both sides it works:
$ scala test . -- '--tests=.*elevation.*'
==> i trail.OtherSuite.other ignored 0.0s
==> i trail.RouteSuite.a route has a start ignored 0.0s
==> i trail.RouteSuite.both tags ignored 0.0s
==> i trail.RouteSuite.untagged ignored 0.0s
==> i trail.RouteSuite.not ready yet ignored 0.0s
==> i trail.RouteSuite.known bug ignored 0.0s
==> i trail.RouteSuite.weather PENDING TRAIL-12 ignored 0.0s
==> i trail.RouteSuite.only on linux ignored 0.0s
trail.RouteSuite:
+ elevation profile 0.01s
Prefer the glob form unless you need a regular expression. The sbt and Mill equivalents are testOnly trail.RouteSuite and ./mill example.test.testOnly trail.RouteSuite,
with the same -- and glob after them (not run here: sbt and Mill are not installed).
Selecting and skipping tests in the code
The test names used in this section come from one suite. The tags are values of munit.Tag, defined in an object for the reason given above:
//> using scala 3.10.0
//> using test.toolkit 0.9.2
package trail
object Tags:
val Offline = munit.Tag("offline")
val Slow = munit.Tag("slow")
import Tags.*
class RouteSuite extends munit.FunSuite:
test("a route has a start".tag(Offline)) {
assert("Ridge".nonEmpty)
}
test("elevation profile".tag(Slow)) {
assertEquals(620 + 120, 740)
}
test("both tags".tag(Offline).tag(Slow)) {
assert(true)
}
test("untagged") {
assert(true)
}
test("not ready yet".ignore) {
fail("never runs")
}
test("known bug".fail) {
assertEquals(1 + 1, 3)
}
test("weather".pending("TRAIL-12")) {
assert(true)
}
test("only on linux") {
assume(scala.util.Properties.isLinux, "this test runs only on Linux")
assert(true)
}
class OtherSuite extends munit.FunSuite:
test("other") {
assert(true)
}
Running everything produced a line for every kind of outcome (macOS here, so assume skipped its test):
trail.OtherSuite:
+ other 0.009s
trail.RouteSuite:
+ a route has a start 0.001s
+ elevation profile 0.002s
+ both tags 0.0s
+ untagged 0.001s
==> i trail.RouteSuite.not ready yet ignored 0.0s
+ known bug 0.012s
==> i trail.RouteSuite.weather PENDING TRAIL-12 ignored 0.0s
==> s trail.RouteSuite.only on linux skipped 0.008s
The modifiers are methods on the test name (they come from an implicit conversion from String to munit.TestOptions) and can be chained:
| Modifier | Effect |
|---|---|
|
Run only this test of the suite (and every other |
|
Skip this test. It is reported as |
|
The test is expected to fail: it passes if its body throws. |
|
A work-in-progress test. The assertions in the body must still pass, but the outcome is reported as ignored with the note ( |
|
Attach a |
|
A test that sometimes fails for reasons outside the code; covered on the next page. |
assume(cond, reason) is not a modifier but a call in the body. When the condition is false the test is reported as skipped (==> s above) instead of failing, which fits
checks that make sense only on one operating system or Scala version. An .only demonstration needs a second run, because it changes what else runs; with test("first".only) added to a
suite shown in the helper section below, only first was reported:
HelperSuite:
+ first 0.009s
An entire suite is skipped with the @munit.IgnoreSuite annotation (JVM only; on Scala.js and Scala Native make the class abstract instead), or by overriding munitIgnore: Boolean
for a condition decided at run time. A class marked with @munit.IgnoreSuite printed nothing at all in the report: it is simply absent.
Running tests by tag
Tags work on the command line with --include-tags and --exclude-tags, which take comma separated tag names (the text passed to munit.Tag, not the Scala value’s name). With the suite above:
Arguments after -- |
Tests that ran |
|---|---|
|
|
|
|
|
|
no arguments |
all tests |
The real report for the first one was:
$ scala test . -- --include-tags=offline
trail.RouteSuite:
+ a route has a start 0.007s
+ both tags 0.0s
and for the combination, the exclusion won over the inclusion of both tags:
$ scala test . -- --include-tags=offline --exclude-tags=slow
trail.RouteSuite:
+ a route has a start 0.004s
Selecting by suite and by tag combine, so scala test . --test-only trail.OtherSuite -- --exclude-tags=slow ran only other.
To skip slow tests by default in sbt, add the flag to the test options, and add a separate configuration or task for the slow ones. The MUnit documentation gives the
testOptions and inConfig lines (not run here):
val MUnitFramework = new TestFramework("munit.Framework")
lazy val myproject = project.settings(
testFrameworks += MUnitFramework,
Test / testOptions += Tests.Argument(MUnitFramework, "--exclude-tags=Slow")
)
The sbt form Test / testOptions is the current syntax; the MUnit page still writes the older testOptions.in(Test). Source:
Scala Toolkit: Use tags to group tests, and run specific tags, MUnit: Include and exclude tests based on tags
Reusing test code with helper functions
A table of cases is just a method that calls test in a loop or from a helper. Declare the implicit location parameter so that a failure points at the call of the helper and not at the assertion line inside
it, which is the same for every case. In Scala 3 it is a using clause (implicit loc: munit.Location in Scala 2):
class HelperSuite extends munit.FunSuite:
def checkHead[A](name: String, xs: List[A], expected: Option[A])(using munit.Location): Unit =
test(name) {
assertEquals(xs.headOption, expected)
}
checkHead("non-empty", List(1, 2), Some(1))
checkHead("empty", List.empty[Int], Some(1))
HelperSuite:
+ non-empty 0.006s
==> X HelperSuite.empty 0.014s munit.ComparisonFailException: H.test.scala:12
11: checkHead("non-empty", List(1, 2), Some(1))
12: checkHead("empty", List.empty[Int], Some(1))
13:
values are not the same
=> Obtained
None
=> Diff (- expected, + obtained)
-Some(
- value = 1
-)
+None
The report shows line 12, the failing case, which is what lets you find it in a table of fifty. Keep slow work out of the helper’s parameters: a value passed by value is evaluated when the suite is
constructed, which is not inside the test, so pass it by name (bytes: => Array[Byte]) when it reads a file or starts a service.
Pitfalls
-
Argument order.
assertEquals(obtained, expected); the report labels sides by that order. Mixing it with JUnit’s(expected, actual)habit makes failures read backwards. -
No assertion, no test. A body that only calls the code passes unless it throws; mark intentional placeholders with
.pending. -
Unrelated types do not compare. The E172 error above; do not silence it with
Anyunless the types really are meant to differ. -
.onlyleft in. It passes the build while skipping the rest of the suite. Search for.onlybefore committing. -
Floating point. Use
assertEqualsDoublewith a delta. -
Top-level definitions in test files with a package. The E222 warning above.
-
Toolkit scope.
//> using toolkitputs MUnit in the main scope; usetest.toolkitfor a library. -
Shared mutable state in the suite body. Tests of one suite share one instance and run in order, but sbt runs suites in parallel by default. Keep state in fixtures (next page) and not in a
varof the class. -
The
--testsregex. It matches the whole name, so it needs.*at the ends.
Where the books are outdated
Books and tutorials written for Scala 2 or the first Scala 3 releases show ScalaTest (FlatSpec, WordSpec, must/should matchers) or specs2 as the default. MUnit is the library of the official Toolkit
and the one the Toolkit tutorials use; the other frameworks remain valid and are summarised on
the next page. The details that date a page are:
-
Toolkit and MUnit version numbers in the dependency lines (0.7.0 and MUnit 1.1.0 in the tutorial; 0.9.2 and 1.2.1 here, 1.3.6 as a standalone library).
-
//> using toolkit latest, deprecated in current Scala CLI in favour ofdefaultor an explicit version. -
The
Diff (- obtained, + expected)legend of older MUnit reports; the current one is- expected, + obtained. -
The Scala 2 flag
-Yrangeposfor clues; Scala 3 does not need it. -
testOptions.in(Test)in older sbt build files;Test / testOptionsis the current sbt slash syntax. -
Tutorials that call
scala-clias a separate binary: thescalacommand of Scala 3 is Scala CLI (see Scala CLI).
See Also
-
Scala CLI for
scala test, scopes and the other directives. -
sbt & Build Tools for test configuration in a full build.
-
Versions, Compatibility & JDKs for which Scala lines and JDKs the tools need.
-
Java Build & Tooling and Groovy: Build Tools for the neighbouring JVM build tools.