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?

What does the runner do?

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.

See dream_test/types.TestInfo for fields and details.

pub type TestInfo =
  types.TestInfo

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 after_all_suites(
  builder builder: RunBuilder(ctx),
  hook hook: fn(List(types.SuiteInfo)) -> Result(Nil, String),
) -> RunBuilder(ctx)

Register a runner-level hook that runs after all suites.

The hook receives the full list of SuiteInfo after filtering. If it returns Error("..."), a synthetic failure result is appended.

pub fn after_each_suite(
  builder builder: RunBuilder(ctx),
  hook hook: fn(types.SuiteInfo) -> Result(Nil, String),
) -> RunBuilder(ctx)

Register a runner-level hook that runs after each suite.

The hook receives types.SuiteInfo for the suite that just ran. If it returns Error("..."), a synthetic failure result is appended.

pub fn after_each_test(
  builder builder: RunBuilder(ctx),
  hook hook: fn(types.TestInfo, ctx) -> Result(Nil, String),
) -> RunBuilder(ctx)

Register a runner-level hook that runs after each test.

The hook receives types.TestInfo and the current context value. If it returns Error("..."), the test is marked Failed.

pub fn before_all_suites(
  builder builder: RunBuilder(ctx),
  hook hook: fn(List(types.SuiteInfo)) -> Result(Nil, String),
) -> RunBuilder(ctx)

Register a runner-level hook that runs before all suites.

The hook receives the full list of SuiteInfo after filtering. If it returns Error("..."), all selected tests are marked Failed and the run short-circuits.

pub fn before_each_suite(
  builder builder: RunBuilder(ctx),
  hook hook: fn(types.SuiteInfo) -> Result(Nil, String),
) -> RunBuilder(ctx)

Register a runner-level hook that runs before each suite.

The hook receives types.SuiteInfo for the suite about to run. If it returns Error("..."), all tests in that suite are marked Failed and the suite is skipped.

pub fn before_each_test(
  builder builder: RunBuilder(ctx),
  hook hook: fn(types.TestInfo, ctx) -> Result(ctx, String),
) -> RunBuilder(ctx)

Register a runner-level hook that runs before each test.

The hook receives types.TestInfo and the current context value. If it returns Error("..."), the test is marked SetupFailed and the test body does not run.

Example

import dream_test/runner
import dream_test/types.{type TestInfo}

fn log_test(info: TestInfo, ctx: Nil) {
  io.println("starting " <> string.join(info.full_name, " > "))
  Ok(ctx)
}

runner.new([suite])
|> runner.before_each_test(log_test)
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 configuring
  • timeout_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(types.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 configuring
  • predicate: 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 of TestResult values returned by runner.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.

  • 1 gives 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 configuring
  • max: maximum number of concurrently running tests (use 1 for 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()).

Search Document