dream_test/runner
Test runner for dream_test.
This module provides a pipe-friendly builder API for running suites and
collecting dream_test/types.TestResult values.
When should I use this?
- Always: the runner is how you execute suites built with
dream_test/unit,dream_test/unit_context, ordream_test/gherkin/feature.
What does the runner do?
- Runs groups sequentially, tests in parallel (bounded by
max_concurrency) - Sandboxes tests and hooks (timeouts + crash isolation)
- Optionally drives an event-based reporter
Example
import dream_test/matchers.{be_equal, or_fail_with, should}
import dream_test/parallel
import dream_test/reporters/bdd
import dream_test/reporters/progress
import dream_test/runner
import dream_test/unit.{describe, it}
pub fn tests() {
describe("Example", [
it("works", fn() {
1 + 1
|> should
|> be_equal(2)
|> or_fail_with("math should work")
}),
])
}
pub fn main() {
let db_config =
parallel.ParallelConfig(max_concurrency: 1, default_timeout_ms: 60_000)
runner.new([])
|> runner.add_suites([tests()])
|> runner.add_suites_with_config(db_config, [db_tests()])
|> runner.progress_reporter(progress.new())
|> runner.results_reporters([bdd.new()])
|> runner.exit_on_failure()
|> runner.run()
}
Types
Output sinks used by runner.run().
Reporters write to out. Runner-internal errors (not test failures) may be
written to error as well.
pub type Output {
Output(out: fn(String) -> Nil, error: fn(String) -> Nil)
}
Constructors
-
Output(out: fn(String) -> Nil, error: fn(String) -> Nil)
Builder for configuring and running suites.
You typically construct one with runner.new(...) and then pipe through
configuration helpers like runner.progress_reporter, runner.results_reporters,
runner.max_concurrency, etc.
Example
import dream_test/matchers.{be_equal, or_fail_with, should}
import dream_test/reporters/bdd
import dream_test/reporters/progress
import dream_test/runner
import dream_test/unit.{describe, it}
import gleam/io
pub fn tests() {
describe("Example", [
it("works", fn() {
1 + 1
|> should
|> be_equal(2)
|> or_fail_with("math should work")
}),
])
}
pub fn main() {
runner.new([tests()])
|> runner.progress_reporter(progress.new())
|> runner.results_reporters([bdd.new()])
|> runner.exit_on_failure()
|> runner.run()
}
pub opaque type RunBuilder(ctx)
Lightweight information about a test, used for filtering what runs.
Fields
name: the test’s local name (the one passed toit("...", ...))full_name: the group path + test name (useful for fully-qualified filters)tags: effective tags (includes inherited group tags)kind: thetypes.TestKind(Unit, Gherkin, etc.)
Example
import dream_test/matchers.{be_equal, or_fail_with, should, succeed}
import dream_test/reporters/bdd
import dream_test/reporters/progress
import dream_test/runner.{type TestInfo}
import dream_test/unit.{describe, it, with_tags}
import gleam/list
pub fn tests() {
describe("Filtering tests", [
it("smoke", fn() {
1 + 1
|> should
|> be_equal(2)
|> or_fail_with("math should work")
})
|> with_tags(["smoke"]),
it("slow", fn() { Ok(succeed()) })
|> with_tags(["slow"]),
])
}
pub fn only_smoke(info: TestInfo) -> Bool {
list.contains(info.tags, "smoke")
}
pub fn main() {
runner.new([tests()])
|> runner.filter_tests(only_smoke)
|> runner.progress_reporter(progress.new())
|> runner.results_reporters([bdd.new()])
|> runner.exit_on_failure()
|> runner.run()
}
pub type TestInfo {
TestInfo(
name: String,
full_name: List(String),
tags: List(String),
kind: types.TestKind,
)
}
Constructors
-
TestInfo( name: String, full_name: List(String), tags: List(String), kind: types.TestKind, )
Values
pub fn add_suites(
builder builder: RunBuilder(ctx),
suites suites: List(types.Root(ctx)),
) -> RunBuilder(ctx)
Append suites to the run, using the builder’s current execution config.
This is useful when you want to build up your suite list incrementally (especially when some suites need a different execution config).
Suites added with add_suites will run using the runner’s current execution
config (configured via max_concurrency / default_timeout_ms).
Example
runner.new([])
|> runner.add_suites([unit_suite()])
|> runner.add_suites([integration_suite()])
|> runner.run()
pub fn add_suites_with_config(
builder builder: RunBuilder(ctx),
config config: parallel.ParallelConfig,
suites suites: List(types.Root(ctx)),
) -> RunBuilder(ctx)
Append suites to the run, using an explicit execution config override.
This lets you run suites with different concurrency/timeout policies in a single runner invocation (for example: DB suites sequential, unit suites parallel).
The override applies only to the suites added by this call. Reporting, output, filtering, and exit behavior remain global runner settings.
Example
import dream_test/parallel
let db_config =
parallel.ParallelConfig(max_concurrency: 1, default_timeout_ms: 60_000)
runner.new([])
|> runner.add_suites([unit_suite()])
|> runner.add_suites_with_config(db_config, [db_suite()])
|> runner.max_concurrency(50)
|> runner.default_timeout_ms(5_000)
|> runner.run()
pub fn default_timeout_ms(
builder builder: RunBuilder(ctx),
timeout_ms timeout_ms: Int,
) -> RunBuilder(ctx)
Set the default timeout (milliseconds) applied to tests without an explicit timeout.
Tests that exceed the timeout are killed and reported as TimedOut.
Example
import dream_test/matchers.{be_equal, or_fail_with, should}
import dream_test/reporters/bdd
import dream_test/reporters/progress
import dream_test/runner
import dream_test/unit.{describe, it}
import gleam/io
pub fn tests() {
describe("Runner config demo", [
it("runs with custom config", fn() {
1 + 1
|> should
|> be_equal(2)
|> or_fail_with("Math works")
}),
])
}
pub fn main() {
runner.new([tests()])
|> runner.max_concurrency(8)
|> runner.default_timeout_ms(10_000)
|> runner.progress_reporter(progress.new())
|> runner.results_reporters([bdd.new()])
|> runner.exit_on_failure()
|> runner.run()
}
Parameters
builder: the runner builder you’re configuringtimeout_ms: timeout in milliseconds applied to tests without an explicit timeout
Returns
The updated RunBuilder(ctx).
pub fn exit_on_failure(
builder builder: RunBuilder(ctx),
) -> RunBuilder(ctx)
Exit the BEAM with a non-zero code if any tests fail.
Useful for CI pipelines.
Example
import dream_test/matchers.{be_equal, or_fail_with, should}
import dream_test/reporters/bdd
import dream_test/reporters/progress
import dream_test/runner
import dream_test/unit.{describe, it}
import gleam/io
pub fn tests() {
describe("Example", [
it("works", fn() {
1 + 1
|> should
|> be_equal(2)
|> or_fail_with("math should work")
}),
])
}
pub fn main() {
runner.new([tests()])
|> runner.progress_reporter(progress.new())
|> runner.results_reporters([bdd.new()])
|> runner.exit_on_failure()
|> runner.run()
}
Parameters
builder: the runner builder you’re configuring
Returns
The updated RunBuilder(ctx).
pub fn filter_tests(
builder builder: RunBuilder(ctx),
predicate predicate: fn(TestInfo) -> Bool,
) -> RunBuilder(ctx)
Filter which tests are executed.
The predicate receives TestInfo (name, full name, effective tags, kind).
Tags include inherited group tags.
Groups with no selected tests in their entire subtree are skipped entirely, including hooks.
Example
import dream_test/matchers.{be_equal, or_fail_with, should, succeed}
import dream_test/reporters/bdd
import dream_test/reporters/progress
import dream_test/runner.{type TestInfo}
import dream_test/unit.{describe, it, with_tags}
import gleam/io
import gleam/list
pub fn tests() {
describe("Filtering tests", [
it("smoke", fn() {
1 + 1
|> should
|> be_equal(2)
|> or_fail_with("math should work")
})
|> with_tags(["smoke"]),
it("slow", fn() { Ok(succeed()) })
|> with_tags(["slow"]),
])
}
pub fn only_smoke(info: TestInfo) -> Bool {
list.contains(info.tags, "smoke")
}
pub fn main() {
runner.new([tests()])
|> runner.filter_tests(only_smoke)
|> runner.progress_reporter(progress.new())
|> runner.results_reporters([bdd.new()])
|> runner.exit_on_failure()
|> runner.run()
}
Parameters
builder: the runner builder you’re configuringpredicate: function that decides whether a test should run
Returns
The updated RunBuilder(ctx).
pub fn has_failures(
results results: List(types.TestResult),
) -> Bool
Return True if the list contains any failing statuses.
This treats Failed, SetupFailed, and TimedOut as failures.
Example
import dream_test/matchers.{be_equal, or_fail_with, should, succeed}
import dream_test/runner
import dream_test/unit.{describe, it}
pub fn tests() {
describe("has_failures", [
it("passes", fn() { Ok(succeed()) }),
])
}
fn failing_suite() {
describe("failing suite", [
it("fails", fn() {
1
|> should
|> be_equal(2)
|> or_fail_with("intentional failure for has_failures example")
}),
])
}
pub fn main() {
let results = runner.new([failing_suite()]) |> runner.run()
results
|> runner.has_failures()
|> should
|> be_equal(True)
|> or_fail_with("expected failures to be present")
}
Parameters
results: list ofTestResultvalues returned byrunner.run
Returns
True when any result has status Failed, SetupFailed, or TimedOut.
pub fn max_concurrency(
builder builder: RunBuilder(ctx),
max max: Int,
) -> RunBuilder(ctx)
Set the maximum number of concurrently running tests.
1gives fully sequential test execution.- Higher values increase parallelism.
Example
import dream_test/matchers.{be_equal, or_fail_with, should}
import dream_test/reporters/bdd
import dream_test/reporters/progress
import dream_test/runner
import dream_test/unit.{describe, it}
import gleam/io
pub fn tests() {
describe("Sequential tests", [
it("first test", fn() {
// When tests share external resources, run them sequentially
1 + 1
|> should
|> be_equal(2)
|> or_fail_with("Math works")
}),
it("second test", fn() {
2 + 2
|> should
|> be_equal(4)
|> or_fail_with("Math still works")
}),
])
}
pub fn main() {
// Sequential execution for tests with shared state
runner.new([tests()])
|> runner.max_concurrency(1)
|> runner.default_timeout_ms(30_000)
|> runner.progress_reporter(progress.new())
|> runner.results_reporters([bdd.new()])
|> runner.exit_on_failure()
|> runner.run()
}
Parameters
builder: the runner builder you’re configuringmax: maximum number of concurrently running tests (use1for fully sequential)
Returns
The updated RunBuilder(ctx).
pub fn new(
suites suites: List(types.Root(ctx)),
) -> RunBuilder(ctx)
Create a new runner builder for a list of suites.
The type parameter ctx is the suite context type. For dream_test/unit
suites this is Nil. For dream_test/unit_context suites it is your custom
context type.
Example
import dream_test/matchers.{be_equal, or_fail_with, should}
import dream_test/reporters/bdd
import dream_test/reporters/progress
import dream_test/runner
import dream_test/unit.{describe, it}
import gleam/io
pub fn tests() {
describe("Example", [
it("works", fn() {
1 + 1
|> should
|> be_equal(2)
|> or_fail_with("math should work")
}),
])
}
pub fn main() {
runner.new([tests()])
|> runner.progress_reporter(progress.new())
|> runner.results_reporters([bdd.new()])
|> runner.exit_on_failure()
|> runner.run()
}
Parameters
suites: the test suites you want to run (often just[tests()])
Returns
A RunBuilder(ctx) you can pipe through configuration helpers and finally
runner.run().
pub fn output(
builder builder: RunBuilder(ctx),
output output: Output,
) -> RunBuilder(ctx)
Configure output sinks for runner.run().
This is how you route reporter output (stdout vs stderr, capturing output in tests, etc).
Example
import dream_test/runner
import gleam/io
pub fn main() {
runner.new([tests()])
|> runner.output(runner.Output(out: io.print, error: io.eprint))
|> runner.run()
}
pub fn progress_reporter(
builder builder: RunBuilder(ctx),
reporter reporter: progress.ProgressReporter,
) -> RunBuilder(ctx)
Attach a progress reporter (live output during the run).
This reporter is driven by TestFinished events in completion order.
It is intended for a single in-place progress bar UI.
pub fn results_reporters(
builder builder: RunBuilder(ctx),
reporters reporters: List(types.ResultsReporter),
) -> RunBuilder(ctx)
Attach results reporters (printed at the end, in the order provided).
Results reporters receive the full traversal-ordered results list from the
RunFinished event, so their output is deterministic under parallel execution.
pub fn run(
builder builder: RunBuilder(ctx),
) -> List(types.TestResult)
Run all suites and return a list of TestResult.
If a progress reporter is attached, the runner will emit progress output during the run. Results reporters print at the end of the run.
Example
import dream_test/matchers.{be_equal, or_fail_with, should}
import dream_test/reporters/bdd
import dream_test/reporters/progress
import dream_test/runner
import dream_test/unit.{describe, it}
pub fn tests() {
describe("Example", [
it("works", fn() {
1 + 1
|> should
|> be_equal(2)
|> or_fail_with("math should work")
}),
])
}
pub fn main() {
runner.new([tests()])
|> runner.progress_reporter(progress.new())
|> runner.results_reporters([bdd.new()])
|> runner.exit_on_failure()
|> runner.run()
}
Parameters
builder: the fully configured runner builder
Returns
A list of TestResult values, in deterministic order.
pub fn silent(
builder builder: RunBuilder(ctx),
) -> RunBuilder(ctx)
Disable all reporter output (still returns results from runner.run()).