Current section
Files
Jump to
Current section
Files
dream_test
README.md
README.md
<div align="center"> <img src="https://raw.githubusercontent.com/TrustBound/dream/main/ricky_and_lucy.png" alt="Dream Logo" width="180"> <h1>Dream Test</h1> <p><strong>A testing framework for Gleam that gets out of your way.</strong></p> <a href="https://hex.pm/packages/dream_test"> <img src="https://img.shields.io/hexpm/v/dream_test?color=8e4bff&label=hex" alt="Hex.pm"> </a> <a href="https://hexdocs.pm/dream_test/"> <img src="https://img.shields.io/badge/docs-hexdocs-8e4bff" alt="Documentation"> </a> <a href="https://github.com/TrustBound/dream_test/blob/main/LICENSE"> <img src="https://img.shields.io/badge/license-MIT-blue" alt="License"> </a></div><br>```gleamimport dream_test/unit.{describe, it}import dream_test/assertions/should.{be_error, be_ok, equal, or_fail_with, should}pub fn tests() { describe("Calculator", [ it("adds two numbers", fn() { add(2, 3) |> should() |> equal(5) |> or_fail_with("2 + 3 should equal 5") }), it("handles division", fn() { divide(10, 2) |> should() |> be_ok() |> equal(5) |> or_fail_with("10 / 2 should equal 5") }), it("returns error for division by zero", fn() { divide(1, 0) |> should() |> be_error() |> or_fail_with("Division by zero should error") }), ])}``````Calculator β adds two numbers β handles division β returns error for division by zero3 tests, 0 failures```<sub>π§ͺ [Tested source](examples/snippets/test/hero.gleam)</sub>---## Installation```toml# gleam.toml[dev-dependencies]dream_test = "~> 1.0"```---## Why Dream Test?| Feature | What you get || ----------------------- | ------------------------------------------------------------------------------ || **Parallel by default** | Tests run concurrently across all coresβ100 tests finish ~4x faster on 4 cores || **Crash-proof** | Each test runs in an isolated BEAM process; one crash doesn't kill the suite || **Timeout-protected** | Hanging tests get killed automatically; no more stuck CI pipelines || **Lifecycle hooks** | `before_all`, `before_each`, `after_each`, `after_all` for setup/teardown || **Gleam-native** | Pipe-first assertions that feel natural; no macros, no reflection, no magic || **Familiar syntax** | If you've used Jest, RSpec, or Mocha, you already know the basics || **Type-safe** | Your tests are just Gleam code; the compiler catches mistakes early || **Self-hosting** | Dream Test tests itself; we eat our own cooking |---## Quick Start### 1. Write tests with `describe` and `it````gleam// test/my_app_test.gleamimport dream_test/unit.{describe, it, to_test_cases}import dream_test/runner.{run_all}import dream_test/reporter/bdd.{report}import dream_test/assertions/should.{should, equal, or_fail_with}import gleam/ioimport gleam/stringpub fn tests() { describe("String utilities", [ it("trims whitespace", fn() { " hello " |> string.trim() |> should() |> equal("hello") |> or_fail_with("Should remove surrounding whitespace") }), it("finds substrings", fn() { "hello world" |> string.contains("world") |> should() |> equal(True) |> or_fail_with("Should find 'world' in string") }), ])}pub fn main() { to_test_cases("my_app_test", tests()) |> run_all() |> report(io.print)}```<sub>π§ͺ [Tested source](examples/snippets/test/quick_start.gleam)</sub>### 2. Run with gleam test```shgleam test```### 3. See readable output```String utilities β trims whitespace β finds substrings2 tests, 0 failures```---## The Assertion PatternEvery assertion follows the same pattern:```gleamvalue |> should() |> matcher() |> or_fail_with("message")```### Chaining matchersMatchers can be chained. Each one passes its unwrapped value to the next:```gleam// Unwrap Some, then check the valueSome(42)|> should()|> be_some()|> equal(42)|> or_fail_with("Should contain 42")// Unwrap Ok, then check the valueOk("success")|> should()|> be_ok()|> equal("success")|> or_fail_with("Should be Ok with 'success'")```<sub>π§ͺ [Tested source](examples/snippets/test/chaining.gleam)</sub>### Available matchers| Category | Matchers || --------------- | ------------------------------------------------------------------------------------------- || **Equality** | `equal`, `not_equal` || **Boolean** | `be_true`, `be_false` || **Option** | `be_some`, `be_none` || **Result** | `be_ok`, `be_error` || **Collections** | `contain`, `not_contain`, `have_length`, `be_empty` || **Comparison** | `be_greater_than`, `be_less_than`, `be_at_least`, `be_at_most`, `be_between`, `be_in_range` || **String** | `start_with`, `end_with`, `contain_string` |### Explicit failuresWhen you need to fail unconditionally:```gleamimport dream_test/assertions/should.{fail_with}case result { Ok(_) -> fail_with("Should have returned an error") Error(_) -> handle_expected_error()}```<sub>π§ͺ [Tested source](examples/snippets/test/explicit_failures.gleam)</sub>### Skipping testsUse `skip` instead of `it` to temporarily disable a test:```gleamimport dream_test/unit.{describe, it, skip}describe("Feature", [ it("works correctly", fn() { ... }), skip("not implemented yet", fn() { ... }), // Skipped it("handles edge cases", fn() { ... }),])``````Feature β works correctly - not implemented yet β handles edge casesSummary: 3 run, 0 failed, 2 passed, 1 skipped```The test body is preserved but not executedβjust change `skip` back to `it` when ready.<sub>π§ͺ [Tested source](examples/snippets/test/skipping_tests.gleam)</sub>---## Lifecycle HooksSetup and teardown logic for your tests. Dream_test supports four lifecycle hooksthat let you run code before and after tests.```gleamimport dream_test/unit.{describe, it, before_each, after_each, before_all, after_all}import dream_test/types.{AssertionOk}describe("Database tests", [ before_all(fn() { start_database() AssertionOk }), before_each(fn() { begin_transaction() AssertionOk }), it("creates a user", fn() { ... }), it("deletes a user", fn() { ... }), after_each(fn() { rollback_transaction() AssertionOk }), after_all(fn() { stop_database() AssertionOk }),])```<sub>π§ͺ [Tested source](examples/snippets/test/lifecycle_hooks.gleam)</sub>### Hook Types| Hook | Runs | Use case || ------------- | --------------------------------- | --------------------------------- || `before_all` | Once before all tests in group | Start services, create temp files || `before_each` | Before each test | Reset state, begin transaction || `after_each` | After each test (even on failure) | Rollback, cleanup temp data || `after_all` | Once after all tests in group | Stop services, remove temp files |### Two Execution ModesChoose the mode based on which hooks you need:| Mode | Function | Hooks supported || ----- | ----------------------------- | --------------------------- || Flat | `to_test_cases` β `run_all` | `before_each`, `after_each` || Suite | `to_test_suite` β `run_suite` | All four hooks |**Flat mode** β simpler, faster; use when you only need per-test setup:```gleamimport dream_test/unit.{describe, it, before_each, to_test_cases}import dream_test/runner.{run_all}to_test_cases("my_test", tests())|> run_all()|> report(io.print)```**Suite mode** β preserves group structure; use when you need once-per-group setup:```gleamimport dream_test/unit.{describe, it, before_all, after_all, to_test_suite}import dream_test/runner.{run_suite}to_test_suite("my_test", tests())|> run_suite()|> report(io.print)```<sub>π§ͺ [Tested source](examples/snippets/test/execution_modes.gleam)</sub>### Hook InheritanceNested `describe` blocks inherit parent hooks. Hooks run outer-to-inner forsetup, inner-to-outer for teardown:```gleamdescribe("Outer", [ before_each(fn() { io.println("1. outer setup") AssertionOk }), after_each(fn() { io.println("4. outer teardown") AssertionOk }), describe("Inner", [ before_each(fn() { io.println("2. inner setup") AssertionOk }), after_each(fn() { io.println("3. inner teardown") AssertionOk }), it("test", fn() { io.println("(test)") AssertionOk }), ]),])// Output: 1. outer setup β 2. inner setup β (test) β 3. inner teardown β 4. outer teardown```<sub>π§ͺ [Tested source](examples/snippets/test/hook_inheritance.gleam)</sub>### Hook Failure BehaviorIf a hook fails, Dream Test handles it gracefully:| Failure in | Result || ------------- | ------------------------------------------------- || `before_all` | All tests in group marked `SetupFailed`, skipped || `before_each` | That test marked `SetupFailed`, skipped || `after_each` | Test result preserved; hook failure recorded || `after_all` | Hook failure recorded; all test results preserved |```gleamdescribe("Handles failures", [ before_all(fn() { case connect_to_database() { Ok(_) -> AssertionOk Error(e) -> fail_with("Database connection failed: " <> e) } }), // If before_all fails, these tests are marked SetupFailed (not run) it("test1", fn() { AssertionOk }), it("test2", fn() { AssertionOk }),])```<sub>π§ͺ [Tested source](examples/snippets/test/hook_failure.gleam)</sub>---## BEAM-Powered Test IsolationEvery test runs in its own BEAM process:| Feature | What it means || ---------------------- | ------------------------------------------------------------ || **Crash isolation** | A `panic` in one test doesn't affect others || **Timeout handling** | Slow tests get killed; suite keeps running || **Parallel execution** | Tests run concurrently (configurable) || **Automatic cleanup** | Resources linked to the test process are freed automatically |```gleam// This test crashes, but others keep runningit("handles edge case", fn() { panic as "oops" // Other tests still execute and report})// This test hangs, but gets killed after timeoutit("fetches data", fn() { infinite_loop() // Killed after 5 seconds (default)})```### Configuring execution```gleamimport dream_test/runner.{run_all_with_config, RunnerConfig}let config = RunnerConfig( max_concurrency: 8, default_timeout_ms: 10_000,)let test_cases = to_test_cases("my_test", tests())run_all_with_config(config, test_cases)|> report(io.print)```<sub>π§ͺ [Tested source](examples/snippets/test/runner_config.gleam)</sub>---## How It WorksDream_test uses an explicit pipelineβno hidden globals, no magic test discovery.### Flat Mode (most common)```describe/it β to_test_cases β run_all β report (DSL) (flatten) (execute) (format)```1. **Define** tests with `describe`/`it` β builds a test tree2. **Convert** with `to_test_cases` β flattens to runnable cases3. **Run** with `run_all` β executes in parallel with isolation4. **Report** with your choice of formatter β outputs results### Suite Mode (for `before_all`/`after_all`)```describe/it β to_test_suite β run_suite β report (DSL) (preserve) (execute) (format)```Suite mode preserves the group hierarchy so hooks can run at group boundaries.### Under the HoodEach test runs in its own BEAM process:```mermaidflowchart TB runner[Test Runner] runner --> t1[Test 1] runner --> t2[Test 2] runner --> t3[Test 3] runner --> t4[Test 4] t1 --> collect[Collect Results] t2 --> collect t3 --> collect t4 --> collect collect --> report[Report]```Benefits:- A crashing test doesn't affect others- Timeouts are enforced via process killing- Resources linked to test processes are cleaned up automatically---## Documentation| Document | Audience || --------------------------------------------- | --------------------------- || **[Hexdocs](https://hexdocs.pm/dream_test/)** | API reference with examples || **[CONTRIBUTING.md](CONTRIBUTING.md)** | How to contribute || **[STANDARDS.md](STANDARDS.md)** | Coding conventions |---## Status**Stable** β v1.0 release. API is stable and ready for production use.| Feature | Status || --------------------------------- | --------- || Core DSL (`describe`/`it`/`skip`) | β
Stable || Lifecycle hooks | β
Stable || Assertions (`should.*`) | β
Stable || BDD Reporter | β
Stable || Parallel execution | β
Stable || Process isolation | β
Stable || Crash handling | β
Stable || Timeout handling | β
Stable || Polling helpers | β
Stable |---## Contributing```shgit clone https://github.com/TrustBound/dream_testcd dream_testmake all # build, test, format```See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow and guidelines.---## LicenseMIT β see [LICENSE.md](LICENSE.md)---<div align="center"> <sub>Part of the <a href="https://github.com/TrustBound/dream">Dream</a> ecosystem for Gleam</sub> <br> <sub>Built in Gleam, on the BEAM, by the Dream Team β€οΈ</sub></div>