MUnit Async, Fixtures & Other Frameworks

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 (3.10.0, released 2026-10-12) with Scala CLI 1.17.1 on JDK 26. Every MUnit and ScalaTest example was compiled and run with scala test on that setup (checked on 2026-10-16) against the Scala Toolkit 0.9.2, which brings MUnit 1.2.1, and the output shown is the output of those runs, with the long stack traces cut. The one example that needs MUnit 1.3.6 (the FutureFixture section) was run with that version. Nothing here is specific to 3.10: the code compiles unchanged on the 3.9 LTS and 3.3 LTS lines. The sbt build files are shown but were not run here: sbt is not installed on the machine used to prepare this page.

Testing with MUnit introduced the Toolkit, suites, assertions, running one test and testing exceptions. This page covers the rest of the official Toolkit testing guide: asynchronous tests, fixtures, and the "what else" features (clues, environment-specific and flaky tests). It then steps outside the Toolkit with ScalaTest, the other widely used test framework, and the sbt tutorial that the Scala documentation uses for it. MUnit itself is a third-party library (scalameta.org/munit) that the Scala Toolkit bundles; ScalaTest (scalatest.org) is also third-party. Property-based tests have their own page, Property-Based Testing. The Java and Groovy counterparts are Testing with JUnit 5 and Mockito and Testing with Spock & Geb.

Getting MUnit

The tests below are Scala CLI test files, so the first lines are directives. A test file is one whose name ends in .test.scala or that lives in a test directory, and test.toolkit puts the Toolkit on the test classpath only:

//> using scala 3.10.0
//> using test.toolkit 0.9.2

To depend on MUnit alone, without the rest of the Toolkit, use //> using test.dep org.scalameta::munit:1.2.1. In sbt the same two choices are "org.scala-lang" %% "toolkit-test" % "0.9.2" % Test or "org.scalameta" %% "munit" % "1.2.1" % Test; in Mill, a test module that mixes in TestModule.Munit. The Toolkit pages show the older versions Toolkit 0.7.0 and MUnit 1.1.0 with Scala 3.4.2; the current Toolkit is 0.9.2, and MUnit has since moved on to 1.3.6, which the Toolkit does not yet bundle. The build-file details are on Scala CLI and sbt & Build Tools.

Asynchronous tests

In Scala an asynchronous operation usually returns a Future (see Futures & Promises). MUnit’s rule is simple: a test body may return a Future[Unit], MUnit waits for it to complete in the background, and the test fails if the future fails. An assertion that throws inside a map or a for comprehension fails the future, and so it fails the test. The Toolkit guide uses an asynchronous square method; the example below adds the failure modes and the timeout.

//> using scala 3.10.0
//> using test.toolkit 0.9.2

import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.duration.*

object AsyncMathLib:
  def square(x: Int)(using ExecutionContext): Future[Int] =
    Future(x * x)

class AsyncMathLibTests extends munit.FunSuite:
  import scala.concurrent.ExecutionContext.Implicits.global

  override val munitTimeout: Duration = 1.second

  test("square") {
    for
      squareOf3 <- AsyncMathLib.square(3)
      squareOfMinus4 <- AsyncMathLib.square(-4)
    yield
      assertEquals(squareOf3, 9)
      assertEquals(squareOfMinus4, 16)
  }

  test("an expected failure of the future") {
    Future.failed[Int](IllegalStateException("closed")).failed.map: error =>
      assertEquals(error.getMessage, "closed")
  }

  test("a failing assertion inside the future") {
    AsyncMathLib.square(5).map(n => assertEquals(n, 26))
  }

  test("a dropped future is never checked") {
    AsyncMathLib.square(5).foreach(n => assertEquals(n, 26))
  }

  test("a future that is too slow") {
    Future:
      Thread.sleep(3000)
  }

Running scala test . printed, with stack frames omitted:

AsyncMathLibTests:
  + square 0.064s
  + an expected failure of the future 0.001s
==> X AsyncMathLibTests.a failing assertion inside the future  0.014s munit.ComparisonFailException: Async.test.scala:31
30:  test("a failing assertion inside the future") {
31:    AsyncMathLib.square(5).map(n => assertEquals(n, 26))
32:  }
values are not the same
=> Obtained
25
=> Diff (- expected, + obtained)
-26
+25
  + a dropped future is never checked 0.001s
==> X AsyncMathLibTests.a future that is too slow  1.003s java.util.concurrent.TimeoutException: test timed out after 1 second
Error: munit tests failed.

Four things to read from that output:

  • The for comprehension in the first test sequences the two futures and then asserts, exactly like the guide. The yield block is the last expression, so its Unit result becomes the Future[Unit] MUnit waits for.

  • To assert that a future fails, turn the failure into a value with failed, which gives a Future[Throwable], and assert on that. intercept takes a by-name block and cannot wait for a future, so it is not the tool here.

  • The "dropped future" test passed, although 26 is wrong. foreach returns Unit immediately, so the test body finished before the assertion ran. The failed assertion was thrown on another thread and Java printed it as an uncaught exception in the middle of the output, which is where the munit.ComparisonFailException trace came from. The rule is to use map/flatMap/for and to return the resulting future, never foreach or a fire-and-forget Future { ... }.

  • munitTimeout is the time MUnit waits for a test to finish; the default is 30 seconds. Overriding it on the suite, as above, changes it for every test in the suite. The slow test failed with a TimeoutException after one second even though its future would still be sleeping.

The execution context in the example is ExecutionContext.Implicits.global, imported inside the class. In Scala 2 the helper takes (implicit ec: ExecutionContext) instead of (using ExecutionContext), and the class body and for comprehension use braces; apart from that the code is the same. The Toolkit guide itself shows the import above the class.

Other result types with value transforms

A test body can return any type as long as MUnit knows how to turn it into a Future. The mechanism is the list munitValueTransforms; the default list already knows Future. To make a type that is not covered acceptable, add a ValueTransform to the list. The example below makes scala.util.Try a legal test result, so that a Failure fails the test:

import scala.util.Try
import scala.concurrent.Future

class TryTests extends munit.FunSuite:
  override def munitValueTransforms =
    super.munitValueTransforms ++ List(
      new ValueTransform("Try", { case t: Try[?] => Future.fromTry(t) })
    )

  test("a Try is a test result too") {
    Try(assertEquals(1, 2))
  }

The "what else" section below runs this transform in a suite, with equal values (the test passed) and with different values (a ComparisonFailException). The MUnit documentation shows the same hook used for a custom effect type such as a Task or an IO, and the third-party effect libraries ship ready-made suites that do this. Source: Scala Toolkit: How to write asynchronous tests? and MUnit: Declare async test.

Fixtures: managing the resources of a test

A fixture creates a resource before a test and releases it afterwards. MUnit has several kinds, and the Toolkit guide recommends the first:

Kind Lifetime Use it when

FunFixture[T]

One resource per test; setup before, teardown after (also after a failure).

Each test needs its own isolated resource: a temporary file, a table, a connection. The guide’s recommended type.

Fixture[T] in munitFixtures

One instance per suite, with beforeAll, beforeEach, afterEach, afterAll hooks.

The resource is expensive to create and tests only read it, such as a server started once.

Ad-hoc fixtures

Overrides of beforeAll, afterAll, beforeEach and afterEach on the suite.

A one-off bit of setup that is not worth a reusable class.

FutureFixture[T] and FunFixture.async

The same lifetimes, with Future-returning setup and teardown.

Setup is asynchronous, for example starting a service and waiting for it.

FunFixture

FunFixture is built from a setup function that receives the TestOptions of the test (so it can use the name) and returns the resource, and a teardown function that releases it. fixture.test("name") { resource => ... } declares a test that receives the resource. FunFixture.map2 combines two fixtures into a FunFixture[(A, B)]. The example records every lifecycle step in a log that the suite prints in afterAll, so the order is visible:

//> using scala 3.10.0
//> using test.toolkit 0.9.2

import scala.collection.mutable.ListBuffer
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

class FixtureTests extends munit.FunSuite:
  val log = ListBuffer.empty[String]

  val usingTempFile: FunFixture[os.Path] = FunFixture(
    setup = options =>
      log += s"setup (${options.name})"
      os.temp(prefix = "file-tests")
    ,
    teardown = tempFile =>
      log += "teardown"
      os.remove(tempFile)
  )

  usingTempFile.test("overwrite on file") { tempFile =>
    os.write.over(tempFile, "Hello, World!")
    assertEquals(os.read(tempFile), "Hello, World!")
  }

  usingTempFile.test("teardown runs after a failure") { tempFile =>
    os.write.over(tempFile, "abc")
    assertEquals(os.size(tempFile), 4L)
  }

  val using2TempFiles: FunFixture[(os.Path, os.Path)] =
    FunFixture.map2(usingTempFile, usingTempFile)

  using2TempFiles.test("merge two files") { (file1, file2) =>
    os.write.over(file1, "a")
    os.write.over(file2, "b")
    assertEquals(os.read(file1) + os.read(file2), "ab")
  }

  val usingAsyncValue: FunFixture[Int] = FunFixture.async(
    setup = _ => Future { log += "async setup"; 21 },
    teardown = _ => Future { log += "async teardown"; () }
  )

  usingAsyncValue.test("async setup and teardown") { n =>
    assertEquals(n * 2, 42)
  }

  // reusable fixture: one instance for the whole suite
  val sharedCounter = new Fixture[Array[Int]]("sharedCounter"):
    private val cell = Array(0)
    def apply(): Array[Int] = cell
    override def beforeAll(): Unit = log += "fixture beforeAll"
    override def beforeEach(context: BeforeEach): Unit = cell(0) += 1
    override def afterEach(context: AfterEach): Unit = log += s"fixture afterEach (${context.test.name})"
    override def afterAll(): Unit = log += "fixture afterAll"

  override def munitFixtures = List(sharedCounter)

  test("the counter counts every test of the suite") {
    assertEquals(sharedCounter()(0), 5)
  }

  override def beforeAll(): Unit = log += "suite beforeAll"
  override def afterAll(): Unit =
    log += "suite afterAll"
    println(log.mkString("\n"))

The output of scala test . was the printed log first, because MUnit prints the report only when the suite is finished, and then the report:

suite beforeAll
fixture beforeAll
setup (overwrite on file)
teardown
fixture afterEach (overwrite on file)
setup (teardown runs after a failure)
teardown
fixture afterEach (teardown runs after a failure)
setup (merge two files)
setup (merge two files)
teardown
teardown
fixture afterEach (merge two files)
async setup
async teardown
fixture afterEach (async setup and teardown)
fixture afterEach (the counter counts every test of the suite)
fixture afterAll
suite afterAll
FixtureTests:
  + overwrite on file 0.024s
==> X FixtureTests.teardown runs after a failure  0.009s munit.ComparisonFailException: Fixtures.test.scala:28
27:    os.write.over(tempFile, "abc")
28:    assertEquals(os.size(tempFile), 4L)
29:  }
values are not the same
=> Obtained
3
=> Diff (- expected, + obtained)
-4
+3
  + merge two files 0.003s
  + async setup and teardown 0.003s
  + the counter counts every test of the suite 0.001s
Error: munit tests failed.

What the run shows:

  • setup ran once per test, and the second test’s teardown ran although its assertion failed (the file contains abc, three bytes, not four). Teardown is the place for cleanup because it always runs. The os.temp and os.remove calls come from the Toolkit’s OS-Lib, see Files & Processes with OS-Lib.

  • FunFixture.map2(usingTempFile, usingTempFile) called setup twice and teardown twice for the one test, and gave the test a tuple. The guide notes that composing a FunFixture[A] and a FunFixture[B] yields a FunFixture[(A, B)].

  • FunFixture.async accepts functions that return a Future, so the setup can wait for something that starts in the background.

  • A Fixture listed in munitFixtures gets its beforeEach and afterEach hooks for every test in the suite, including tests that do not use it, which is why the counter reads 5 in the last test: the five tests before and including it each incremented it. The suite-level beforeAll/afterAll ran outermost, the fixture hooks inside them.

  • The FunFixture setup and teardown are invoked around the body of the one test that uses the fixture; the reusable fixture’s hooks surround every test.

The guide gives the reason to prefer FunFixture: it is explicit (each test declares the resource it needs) and safe (each test has its own resource, so tests cannot interfere). The shared state of a Fixture is mutable, so keep it for read-mostly resources. Create expensive resources in beforeAll, not in the class constructor, as the MUnit documentation advises, because the constructor runs even when the suite is filtered out. Source: Scala Toolkit: How to manage the resources of a test? and MUnit: Fixtures.

Asynchronous fixtures with FutureFixture

For lifecycle hooks that return a Future, MUnit 1.3.6 adds FutureFixture[T], a Fixture whose beforeAll, beforeEach, afterEach and afterAll return Future[Unit]. The MUnit documentation marks it as available only in 1.3.6. That version is newer than the one the Toolkit 0.9.2 pins (MUnit 1.2.1), so it needs an explicit dependency, //> using test.dep org.scalameta::munit:1.3.6, instead of test.toolkit. This example was run with 1.3.6:

//> using scala 3.10.0
//> using test.dep org.scalameta::munit:1.3.6

import munit.FutureFixture
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

class FutureFixtureTests extends munit.FunSuite:
  val connection = new FutureFixture[String]("connection"):
    private var value = "closed"
    def apply(): String = value
    override def beforeAll(): Future[Unit] = Future { Thread.sleep(50); value = "open" }
    override def afterAll(): Future[Unit] = Future { value = "closed" }

  override def munitFixtures = List(connection)

  test("the asynchronous fixture was opened before the test") {
    assertEquals(connection(), "open")
  }
Test run FutureFixtureTests started
FutureFixtureTests: finished 0.071s
Test run FutureFixtureTests finished: 0 failed, 0 ignored, 1 total 0.075s

The report format differs from the MUnit 1.2.1 runs above; the test passed. On the Toolkit’s MUnit 1.2.1 the equivalent is FunFixture.async for per-test resources, or doing the waiting yourself inside a synchronous beforeAll. Source: MUnit: Fixtures.

What else can MUnit do?

The last Toolkit testing page lists three smaller features. All three appear in the suite below, together with the neighbours the MUnit documentation puts next to them (ignore, fail):

//> using scala 3.10.0
//> using test.toolkit 0.9.2

import scala.util.{Properties, Try}
import scala.concurrent.Future

class EnvironmentTests extends munit.FunSuite:
  val a = 1
  val b = 2

  test("a clue explains a failed assertion") {
    assert(clue(List(a).head) > clue(b))
  }

  test("only on Linux") {
    assume(Properties.isLinux, "this test runs only on Linux")
    assert(os.home.toString.startsWith("/home/"))
  }

  test("only when a variable is set") {
    assume(sys.env.contains("TRAILHEAD_CI"), "TRAILHEAD_CI is not set")
    assert(true)
  }

  test("requests".flaky) {
    assertEquals(a, b)
  }

  test("work in progress".ignore) {
    assertEquals(a, b)
  }

  test("known bug".fail) {
    assertEquals(a, b)
  }

On macOS, without the variable, scala test . printed (stack frames omitted):

EnvironmentTests:
==> X EnvironmentTests.a clue explains a failed assertion  0.029s munit.FailException: Env.test.scala:12 assertion failed
11:  test("a clue explains a failed assertion") {
12:    assert(clue(List(a).head) > clue(b))
13:  }
Clues {
  List(a).head: Int = 1
  b: Int = 2
}
==> s EnvironmentTests.only on Linux skipped 0.008s
==> s EnvironmentTests.only when a variable is set skipped 0.003s
==> X EnvironmentTests.requests  0.008s munit.ComparisonFailException: Env.test.scala:26
25:  test("requests".flaky) {
26:    assertEquals(a, b)
27:  }
values are not the same
=> Obtained
1
=> Diff (- expected, + obtained)
-2
+1
==> i EnvironmentTests.work in progress ignored 0.0s
  + known bug 0.002s
Error: munit tests failed.

Clues

A plain assert(x > y) only says that the assertion failed. Wrapping the sub-expressions in clue(...) makes MUnit print their source text, type and value in a Clues block, as above (List(a).head: Int = 1). clue works inside assert and the other assertion methods, on Scala 2 and Scala 3 alike.

Environment-specific tests with assume

assume(condition, message) skips the test when the condition is false instead of failing it. The report marks the test with s and the word skipped. Any boolean can be used: the operating system (Properties.isLinux, Properties.isMac), the Java version, a system property or, as in the second test, an environment variable. Run with TRAILHEAD_CI=1, the second test is executed. Selecting one test from the command line is a regular expression passed to MUnit after --, with the variable set:

TRAILHEAD_CI=1 scala test . -- '--tests=.*when a variable.*'
EnvironmentTests:
  + only when a variable is set 0.02s

The other tests of the suite appear as ignored in that run. Without any filter, tests can also be excluded for a whole suite, by a tag or by a dynamic condition (munitIgnore), which the MUnit page on filtering describes. The Toolkit guide’s own example asserts that os.home starts with /home/ and is therefore skipped on a Mac; the version above keeps the same shape.

Flaky tests

"name".flaky marks a test as flaky. A flaky test that fails does not break the build when the environment variable MUNIT_FLAKY_OK is true; the test is reported as skipped. Run with the variable set, the same suite printed:

==> s EnvironmentTests.requests skipped 0.007s

Without it the failure above is reported like any other. Flaky is a quarantine, not a fix: it fits the I/O-heavy tests that fail on a busy build server, and the tag should come with a ticket. MUnit can also re-evaluate a body until its assertions stop failing with eventually(), documented under "Retry failing assertions". The related markers are .ignore, which skips the test and reports it as ignored (==> i), and .fail, which inverts the result: a test that should fail passes while the bug exists and starts failing, with expected failure but test passed, when somebody fixes it. That is a convenient way to pin a known bug.

Tests that return a Try

The ValueTransform from the earlier section was added to the EnvironmentTests suite as one more test, a Try is a test result too, which passed with equal values. With Try(assertEquals(a, b)) in its place the test failed with

==> X EnvironmentTests.a Try is a test result too  0.002s munit.ComparisonFailException: Env.test.scala:38
values are not the same
=> Obtained
1
=> Diff (- expected, + obtained)
-2
+1

so the failure inside the Try was not swallowed. Source: Scala Toolkit: What else can MUnit do?, MUnit: Tests and MUnit: Filtering tests.

Other test frameworks

MUnit is the framework the Toolkit chose, but it is not the only one, and a project you join may use another. The official "Testing Scala with sbt" tutorial uses ScalaTest, and its closing paragraph points at ScalaCheck and Specs2 as further options. ScalaTest is the best known. Its styles are chosen by the suite you extend (AnyFunSuite for the test("...") style, plus AnyFlatSpec, AnyWordSpec, AnyFreeSpec, AnyFunSpec, AnyFeatureSpec and AnyPropSpec; the user guide’s "Selecting a style" page compares them), and its assertions come from the Assertions, Matchers and Inspectors traits.

ScalaTest with Scala CLI

ScalaTest (here 3.2.20, published 2026-03-25 on Maven Central) is a normal library for Scala CLI. There is no Toolkit flag for it, only a dependency, restricted to the test scope with test.dep. The example is the tutorial’s CubeCalculator in two files. Cube.scala:

//> using scala 3.10.0

object CubeCalculator:
  def cube(x: Int): Int = x * x * x

and CubeCalculatorTest.test.scala, with a deliberately wrong last expectation (-2 cubed is -8):

//> using test.dep org.scalatest::scalatest:3.2.20

import org.scalatest.funsuite.AnyFunSuite

class CubeCalculatorTest extends AnyFunSuite:
  test("CubeCalculator.cube 3 should be 27") {
    assert(CubeCalculator.cube(3) === 27)
  }

  test("CubeCalculator.cube 0 should be 0") {
    assert(CubeCalculator.cube(0) === 0)
  }

  test("CubeCalculator.cube -2 should be -9") {
    assert(CubeCalculator.cube(-2) === -9)
  }

scala test . downloaded ScalaTest from Maven Central and printed:

CubeCalculatorTest:
- CubeCalculator.cube 3 should be 27
- CubeCalculator.cube 0 should be 0
- CubeCalculator.cube -2 should be -9 *** FAILED ***
  -8 did not equal -9 (CubeCalculatorTest.test.scala:15)
Run completed in 76 milliseconds.
Total number of tests run: 3
Suites: completed 1, aborted 0
Tests: succeeded 2, failed 1, canceled 0, ignored 0, pending 0
*** 1 TEST FAILED ***
Error: ScalaTest tests failed.

Scala CLI detects the framework on the classpath, so the same scala test command runs MUnit, ScalaTest and the other frameworks that implement the sbt test interface. In the tutorial’s code === is ScalaTest’s equality check, which gives the message -8 did not equal -9; the plain == also works but the message is less informative. ScalaTest tests that return a Future need AsyncFunSuite; they do not share MUnit’s rule that any test body may return one.

The sbt track

The Scala documentation’s getting-started tutorial for tests, Testing Scala with sbt and ScalaTest on the Command Line, follows the sbt route: sbt new scala/scalatest-example.g8 creates a project named ScalaTestTutorial, sbt test runs the generated CubeCalculatorTest, and the tutorial then adds a second test and runs sbt test again. It is the same code as above. The relevant part of the build file is:

// build.sbt (not run here: sbt is not installed on the machine used to prepare this page)
scalaVersion := "3.10.0"

libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.20" % Test

and the tutorial’s output is the same list of test names followed by All tests passed., Tests: succeeded 2, failed 0 and the elapsed time. Three details of that page are out of date and should not be copied: the sample output shows sbt 0.13 and [info] Loading global plugins from /Users/.../.sbt/0.13/plugins; the test class uses the Scala 2 brace style; and the second output shows a scala-2.13 build directory. The sbt tutorial and the general build tooling are covered on sbt & Build Tools.

Choosing a framework

Framework Character Use it when

MUnit

Small, minimal API; fixtures and Future tests built in; the framework of the Toolkit and of Scala CLI’s documentation.

You start a new project or a script and want the default.

ScalaTest

Many test styles, rich matchers, long history, large user base.

The project already uses it, or you need its matcher DSL or a particular style.

ScalaCheck (see Property-Based Testing)

Property-based testing: generated inputs and shrinking.

You want to test invariants rather than examples; it combines with MUnit or ScalaTest.

Specs2, and effect-library suites

Specification-style tests, or suites for a particular effect system.

The project is built around that library. They are named here as pointers and are not covered further.

For the JUnit-based path in Java, see Java Build & Tooling; the Groovy equivalent is Testing with JUnit & Groovy Test.

Where the sources are outdated

  • The Toolkit testing pages show //> using toolkit latest, toolkit-test 0.7.0, MUnit 1.1.0 and Scala 3.4.2. Use Toolkit 0.9.2 (MUnit 1.2.1), or MUnit 1.3.6 directly, and a pinned Scala version such as 3.10.0 in place of latest.

  • The Toolkit guide’s munitFixtures, FunFixture and assume examples are unchanged and run as written on 3.10.0. FutureFixture is documented by MUnit only for 1.3.6 and does not exist in the Toolkit’s MUnit 1.2.1.

  • The sbt/ScalaTest tutorial shows ScalaTest with sbt 0.13 output and Scala 2 braces, as noted above. The dependency to use today is scalatest 3.2.x.

  • The Scala 2 forms of the examples use braces after the class and implicit in place of using; everything else is the same.

Pitfalls

  • Dropping the future with foreach or a bare Future { ... } makes the test pass without checking anything. Return the future.

  • A fixture’s beforeEach/afterEach runs for every test in the suite, not only for the ones that use it.

  • Starting a resource in the suite constructor runs it even for tests that are filtered out; use beforeAll.

  • .flaky hides failures only when MUNIT_FLAKY_OK is set; a CI job that sets it permanently hides real regressions.

  • Filter patterns passed with --tests= are regular expressions, not globs: *when* fails with Dangling meta character '*'; write .*when.*.