Skip to main content

Running tests

A test project in bleep is an ordinary project with isTestProject: true. There is no separate test scope and no second source root to configure — tests are a project that depends on the code under test:

projects:
myapp: {}
myapp-test:
isTestProject: true
dependsOn: myapp
dependencies:
- org.scalatest::scalatest:3.2.19
bleep test # every test project
bleep test myapp-test # one project (deps compile first)
bleep test -w # watch mode: re-run on change

bleep test with no project argument runs every test project; naming non-test projects just compiles them. Test frameworks are auto-detected from the classpath — ScalaTest, JUnit 4/5 (JUnit Platform), MUnit, utest, Specs2, ScalaCheck, and weaver all work without configuration.

How a project's suites run

Two knobs, each answering one question, plus one rule:

SettingQuestion it answersDefault
testForkWhere do a project's suites run?per-project (one shared JVM)
maxConcurrentSuitesHow many run at once?1 — sequential

The rule: in a shared JVM, only JUnit Platform parallelizes; for sbt-interface frameworks, concurrency means separate forks (per-suite). Everything below follows from it.

testFork: per-project (the default)

Every one of a project's suites runs in one forked JVM — the same model as maven surefire's forkCount=1 reuseForks=true. JVM-wide state is built once and reused across suites: a booted application, dev-service containers, a shared Testcontainers instance, a schema an earlier suite created. Suites run one at a time by default (maxConcurrentSuites: 1), which is memory-frugal — one live suite's heap at a time — and correct for every framework. Your cores stay busy because bleep runs many projects' forks at once (bounded by the machine-wide governor), not many suites within one fork.

myapp-test:
isTestProject: true
# testFork: per-project # implied
# maxConcurrentSuites: 1 # implied (sequential)

In per-project mode, maxConcurrentSuites only affects JUnit Platform suites — it runs that many of the project's JUnit classes at once in the one fork. sbt-interface frameworks always run sequentially here (they share one Runner); set it on an sbt-only project and bleep warns it has no effect. Either way it bounds this project's concurrency — the machine-wide governor still caps the total across all projects.

One caveat: bleep runs each class as its own JUnit execution today, so @ResourceLock is not coordinated across classes — keep it at 1 when a project's classes share mutable state. (The full sbt-vs-JUnit story is in Test frameworks.)

web-test:
isTestProject: true
maxConcurrentSuites: 4 # up to 4 of this project's JUnit classes at once

Under the hood, per-project runs each suite as its own execution on a context built once for the fork: for JUnit Platform, one launcher execution per class on a single LauncherSession — so a session-scoped fixture (a booted application, a LauncherSessionListener) is set up once; for sbt frameworks, all the suites through one Runner with a single done(). This is automatic; you never configure it. See Framework support below.

testFork: per-suite

Each suite runs in its own forked JVM, pooled by classpath. Suites are isolated by the operating system: one that calls System.exit, wedges a thread, or corrupts a static kills a process bleep can replace, without touching the others. This is also how you run sbt-interface suites concurrently — a fork each, safely isolated. maxConcurrentSuites then bounds how many of this project's forks run at once (unset = unbounded); the machine-wide governor still caps the total across all projects.

flaky-tests:
isTestProject: true
testFork: per-suite # OS-level isolation, one suite can't poison another
scalatest-test:
isTestProject: true
testFork: per-suite # concurrency for sbt suites: a fork each
maxConcurrentSuites: 4 # up to 4 forks at once

Choosing between them

Reach forWhen
per-project (default)Normal unit tests; anything that boots an app or shares expensive fixtures (Testcontainers, an embedded DB). Fewer JVM starts, warm reuse, one live suite's heap at a time.
per-project + maxConcurrentSuites > 1JUnit Platform suites you want to run concurrently in that one JVM (that many classes at once). No effect on sbt frameworks.
per-suiteSuites that must not share a JVM (System.exit, thread leaks, static corruption), or sbt-interface suites you want to run concurrently.

Framework support: sbt test interface vs JUnit Platform

Bleep discovers and runs suites through two runner surfaces, and both execution modes apply to both:

  • sbt test interface — ScalaTest, MUnit, utest, Specs2, ScalaCheck, weaver, ZIO Test, and the rest of the Scala ecosystem.
  • JUnit Platform — JUnit 5 and the engines built on it (Cucumber, Spek, jqwik, …).

What per-project does for each, and why it's correct:

per-suiteper-project
sbt interfaceone fork per suiteall the project's suites of one framework through one Runner, one done() in the shared fork — the interface's one-runner-per-framework contract, so stateful frameworks (munit, ZIO) behave. Always sequentialmaxConcurrentSuites does not apply
JUnit Platformone fork per suiteeach class its own execution on one shared LauncherSession in the shared fork — session-scoped fixtures (a booted application, a LauncherSessionListener) build once, and per-class attribution is exact. maxConcurrentSuites runs that many classes at once

In per-project mode, maxConcurrentSuites raises concurrency only for JUnit Platform, because JUnit's tests are the ones written for parallel execution; sbt frameworks share one Runner and run one suite at a time. To run sbt suites concurrently, use testFork: per-suite (a fork each). A framework that reports a suite's failure detail only from its once-per-run done() can't share a Runner at all; bleep gives such frameworks their own fork per suite automatically (weaver, hedgehog today). And the machine-wide governor bounds the total across all projects, so bleep test never oversubscribes your cores.

JVM options and heap

Options for the forked test JVM come from three places, applied in order:

  1. the project's platform.jvmOptions (also picked up from maven's surefire argLine on import);
  2. any fork options a sourcegen step declared for the project;
  3. --jvm-opt on the command line.
bleep test myapp-test --jvm-opt -Dmy.flag=true --jvm-opt -Duser.timezone=UTC

The per-fork heap defaults to a fixed size; change it once for the whole build with bleep config test-runner-heap <size> (and test-runner-heap-clear to reset). A project -Xmx in platform.jvmOptions still wins for that project's fork.

Selecting what runs

bleep test --only FooSpec # suites whose name matches (substring, or FQDN to disambiguate)
bleep test --exclude SlowSpec # drop matching suites (takes precedence over --only)
bleep test --test-arg -oD # pass arguments straight to the framework

For tag-based selection across frameworks (--only-tag, --exclude-tag, and the project pre-filter that skips compiling untagged projects), see Test tags.

Reports and history

  • --junit-report <dir> writes one JUnit XML per suite, for CI test-reporting UIs.
  • Every run is recorded as a transcript; bleep test --diff shows only what changed versus a previous run. See Run history & diffs.

Reference

FieldTypeMeaning
isTestProjecttrueMarks the project as tests; turns on suite discovery.
testForkper-project | per-suiteFork granularity. Default per-project.
maxConcurrentSuitesintegerCeiling on this project's suites at once (default 1); the machine-wide governor caps the total across all projects. Per-project: JUnit classes inside the one fork (no effect on sbt frameworks — they stay sequential). Per-suite: number of forks (default unbounded).
testFrameworkslistEscape hatch to name an sbt test-interface Framework class bleep doesn't auto-detect; normally unset. JUnit Platform engines are found via the platform SPI and cannot be named here.
testTagsmapTag → suite class-name patterns for --only-tag/--exclude-tag. See Test tags.