Current section
Files
Jump to
Current section
Files
src/dream_test/unit.gleam
//// Unit test DSL for dream_test.
////
//// This module provides a BDD-style syntax for defining tests: `describe`,
//// `it`, and lifecycle hooks (`before_all`, `before_each`, `after_each`,
//// `after_all`). Tests are organized hierarchically and converted to
//// runnable test cases or suites.
////
//// ## Quick Start
////
//// ```gleam
//// import dream_test/unit.{describe, it, to_test_cases}
//// import dream_test/assertions/should.{should, equal, or_fail_with}
//// import dream_test/runner.{run_all}
//// import dream_test/reporter/bdd.{report}
//// import gleam/io
////
//// pub fn main() {
//// tests()
//// |> to_test_cases("my_module_test")
//// |> run_all()
//// |> report(io.print)
//// }
////
//// pub fn tests() {
//// describe("Calculator", [
//// describe("add", [
//// it("adds positive numbers", fn() {
//// add(2, 3)
//// |> should()
//// |> equal(5)
//// |> or_fail_with("2 + 3 should equal 5")
//// }),
//// it("handles zero", fn() {
//// add(0, 5)
//// |> should()
//// |> equal(5)
//// |> or_fail_with("0 + 5 should equal 5")
//// }),
//// ]),
//// ])
//// }
//// ```
////
//// ## Output
////
//// ```text
//// Calculator
//// add
//// ✓ adds positive numbers
//// ✓ handles zero
////
//// Summary: 2 run, 0 failed, 2 passed
//// ```
////
//// ## Lifecycle Hooks
////
//// Setup and teardown logic for tests:
////
//// ```gleam
//// import dream_test/unit.{describe, it, before_each, after_each, to_test_cases}
//// import dream_test/types.{AssertionOk}
////
//// describe("Database", [
//// before_each(fn() {
//// reset_database()
//// AssertionOk
//// }),
////
//// it("creates users", fn() { ... }),
//// it("queries users", fn() { ... }),
////
//// after_each(fn() {
//// rollback()
//// AssertionOk
//// }),
//// ])
//// ```
////
//// | Hook | Runs | Requires |
//// |---------------|-----------------------------------|-------------------|
//// | `before_all` | Once before all tests in group | `to_test_suite` |
//// | `before_each` | Before each test | Either mode |
//// | `after_each` | After each test (always) | Either mode |
//// | `after_all` | Once after all tests in group | `to_test_suite` |
////
//// ## Two Execution Modes
////
//// **Flat mode** — faster, simpler, no `before_all`/`after_all`:
////
//// ```gleam
//// tests() |> to_test_cases("my_test") |> run_all()
//// ```
////
//// **Suite mode** — supports all hooks, preserves group structure:
////
//// ```gleam
//// tests() |> to_test_suite("my_test") |> run_suite()
//// ```
////
//// ## Nesting
////
//// You can nest `describe` blocks as deeply as needed. Each level adds to
//// the test's `full_name`, which the reporter uses for grouping output.
//// Lifecycle hooks are inherited by nested groups.
////
//// ```gleam
//// describe("User", [
//// before_each(fn() { create_user(); AssertionOk }),
////
//// describe("authentication", [
//// describe("with valid credentials", [
//// it("returns the user", fn() { ... }),
//// it("sets the session", fn() { ... }),
//// ]),
//// describe("with invalid credentials", [
//// it("returns an error", fn() { ... }),
//// ]),
//// ]),
//// ])
//// ```
import dream_test/types.{
type AssertionResult, type TestCase, type TestSuite, type TestSuiteItem,
SingleTestConfig, SuiteGroup, SuiteTest, TestCase, TestSuite, Unit,
}
import gleam/list
import gleam/option.{None}
/// A node in the test tree.
///
/// This type represents either a single test (`ItTest`), a group of tests
/// (`DescribeGroup`), or a lifecycle hook. You typically don't construct
/// these directly—use `it`, `describe`, and the hook functions instead.
///
/// ## Variants
///
/// - `ItTest(name, run)` - A single test with a name and body function
/// - `DescribeGroup(name, children)` - A group of tests under a shared name
/// - `BeforeAll(setup)` - Runs once before all tests in the group
/// - `BeforeEach(setup)` - Runs before each test in the group
/// - `AfterEach(teardown)` - Runs after each test in the group
/// - `AfterAll(teardown)` - Runs once after all tests in the group
///
pub type UnitTest {
ItTest(name: String, run: fn() -> AssertionResult)
DescribeGroup(name: String, children: List(UnitTest))
BeforeAll(setup: fn() -> AssertionResult)
BeforeEach(setup: fn() -> AssertionResult)
AfterEach(teardown: fn() -> AssertionResult)
AfterAll(teardown: fn() -> AssertionResult)
}
/// Define a single test case.
///
/// The test body is a function that returns an `AssertionResult`. Use the
/// `should` API to build assertions that produce this result.
///
/// ## Example
///
/// ```gleam
/// it("calculates the sum correctly", fn() {
/// add(2, 3)
/// |> should()
/// |> equal(5)
/// |> or_fail_with("Expected 2 + 3 to equal 5")
/// })
/// ```
///
/// ## Naming Conventions
///
/// Good test names describe the expected behavior:
/// - ✓ "returns the user when credentials are valid"
/// - ✓ "rejects empty passwords"
/// - ✗ "test1"
/// - ✗ "works"
///
pub fn it(name: String, run: fn() -> AssertionResult) -> UnitTest {
ItTest(name, run)
}
/// Group related tests under a common description.
///
/// Groups can be nested to any depth. The group names form a hierarchy that
/// appears in test output and failure messages.
///
/// ## Example
///
/// ```gleam
/// describe("String utilities", [
/// describe("trim", [
/// it("removes leading spaces", fn() { ... }),
/// it("removes trailing spaces", fn() { ... }),
/// ]),
/// describe("split", [
/// it("splits on delimiter", fn() { ... }),
/// ]),
/// ])
/// ```
///
/// ## Output
///
/// ```text
/// String utilities
/// trim
/// ✓ removes leading spaces
/// ✓ removes trailing spaces
/// split
/// ✓ splits on delimiter
/// ```
///
pub fn describe(name: String, children: List(UnitTest)) -> UnitTest {
DescribeGroup(name, children)
}
/// Run setup once before all tests in the current `describe` block.
///
/// Use `before_all` when you have expensive setup that should happen once
/// for the entire group rather than before each individual test.
///
/// ## When to Use
///
/// - Starting a database server
/// - Creating temporary files or directories
/// - Launching external services
/// - Any setup that's slow or has side effects you want to share
///
/// ## Execution Behavior
///
/// - Runs exactly once, before the first test in the group
/// - If it returns `AssertionFailed`, all tests in the group are skipped
/// and marked as `SetupFailed`
/// - Nested `describe` blocks each run their own `before_all` hooks
///
/// ## Example
///
/// ```gleam
/// import dream_test/unit.{describe, it, before_all, after_all, to_test_suite}
/// import dream_test/runner.{run_suite}
/// import dream_test/types.{AssertionOk}
///
/// describe("Database integration", [
/// before_all(fn() {
/// // This runs once before any test
/// start_test_database()
/// run_migrations()
/// AssertionOk
/// }),
///
/// it("creates users", fn() { ... }),
/// it("queries users", fn() { ... }),
/// it("updates users", fn() { ... }),
///
/// after_all(fn() {
/// stop_test_database()
/// AssertionOk
/// }),
/// ])
/// |> to_test_suite("db_test")
/// |> run_suite()
/// ```
///
/// ## Important: Requires Suite Mode
///
/// `before_all` hooks only work with `to_test_suite` + `run_suite`.
/// When using `to_test_cases` + `run_all`, they are silently ignored.
///
/// This is because flat mode loses the group structure needed to know
/// where "all tests in a group" begins and ends.
///
pub fn before_all(setup: fn() -> AssertionResult) -> UnitTest {
BeforeAll(setup)
}
/// Run setup before each test in the current `describe` block.
///
/// Use `before_each` when tests need a fresh, isolated state. This is the
/// most commonly used lifecycle hook.
///
/// ## When to Use
///
/// - Resetting database state between tests
/// - Creating fresh test fixtures
/// - Beginning a transaction to rollback later
/// - Clearing caches or in-memory state
///
/// ## Execution Behavior
///
/// - Runs before every test in the group and all nested groups
/// - If it returns `AssertionFailed`, the test is skipped and marked `SetupFailed`
/// - Multiple `before_each` hooks in the same group run in declaration order
///
/// ## Example
///
/// ```gleam
/// import dream_test/unit.{describe, it, before_each, after_each, to_test_cases}
/// import dream_test/runner.{run_all}
/// import dream_test/types.{AssertionOk}
///
/// describe("Shopping cart", [
/// before_each(fn() {
/// // Fresh cart for each test
/// clear_cart()
/// AssertionOk
/// }),
///
/// it("starts empty", fn() {
/// get_cart_items()
/// |> should()
/// |> equal([])
/// |> or_fail_with("New cart should be empty")
/// }),
///
/// it("adds items", fn() {
/// add_to_cart("apple")
/// get_cart_items()
/// |> should()
/// |> contain("apple")
/// |> or_fail_with("Cart should contain apple")
/// }),
/// ])
/// |> to_test_cases("cart_test")
/// |> run_all()
/// ```
///
/// ## Hook Inheritance
///
/// Nested `describe` blocks inherit parent `before_each` hooks. Parent hooks
/// run first (outer-to-inner order):
///
/// ```gleam
/// describe("Outer", [
/// before_each(fn() { setup_outer(); AssertionOk }), // Runs 1st
///
/// describe("Inner", [
/// before_each(fn() { setup_inner(); AssertionOk }), // Runs 2nd
/// it("test", fn() { ... }),
/// ]),
/// ])
/// ```
///
/// ## Works in Both Modes
///
/// Unlike `before_all`, `before_each` works with both `to_test_cases`
/// and `to_test_suite`. Use whichever fits your needs.
///
pub fn before_each(setup: fn() -> AssertionResult) -> UnitTest {
BeforeEach(setup)
}
/// Run teardown after each test in the current `describe` block.
///
/// Use `after_each` to clean up resources created during a test. This hook
/// runs even if the test fails, ensuring reliable cleanup.
///
/// ## When to Use
///
/// - Rolling back database transactions
/// - Deleting temporary files created by the test
/// - Resetting global state or mocks
/// - Closing connections or releasing resources
///
/// ## Execution Behavior
///
/// - Runs after every test in the group and all nested groups
/// - **Always runs**, even if the test or `before_each` hooks fail
/// - Multiple `after_each` hooks in the same group run in reverse declaration order
///
/// ## Example
///
/// ```gleam
/// import dream_test/unit.{describe, it, before_each, after_each, to_test_cases}
/// import dream_test/runner.{run_all}
/// import dream_test/types.{AssertionOk}
///
/// describe("File operations", [
/// before_each(fn() {
/// create_temp_directory()
/// AssertionOk
/// }),
///
/// after_each(fn() {
/// // Always clean up, even if test crashes
/// delete_temp_directory()
/// AssertionOk
/// }),
///
/// it("writes files", fn() { ... }),
/// it("reads files", fn() { ... }),
/// ])
/// |> to_test_cases("file_test")
/// |> run_all()
/// ```
///
/// ## Hook Inheritance
///
/// Nested `describe` blocks inherit parent `after_each` hooks. Child hooks
/// run first (inner-to-outer order, reverse of `before_each`):
///
/// ```gleam
/// describe("Outer", [
/// after_each(fn() { teardown_outer(); AssertionOk }), // Runs 2nd
///
/// describe("Inner", [
/// after_each(fn() { teardown_inner(); AssertionOk }), // Runs 1st
/// it("test", fn() { ... }),
/// ]),
/// ])
/// ```
///
/// ## Works in Both Modes
///
/// Like `before_each`, `after_each` works with both `to_test_cases`
/// and `to_test_suite`.
///
pub fn after_each(teardown: fn() -> AssertionResult) -> UnitTest {
AfterEach(teardown)
}
/// Run teardown once after all tests in the current `describe` block.
///
/// Use `after_all` to clean up expensive resources that were set up by
/// `before_all`. This hook runs once after all tests complete, regardless
/// of whether tests passed or failed.
///
/// ## When to Use
///
/// - Stopping a database server started by `before_all`
/// - Removing temporary directories
/// - Shutting down external services
/// - Any cleanup that corresponds to `before_all` setup
///
/// ## Execution Behavior
///
/// - Runs exactly once, after the last test in the group completes
/// - **Always runs**, even if tests fail or `before_all` fails
/// - Nested `describe` blocks each run their own `after_all` hooks
///
/// ## Example
///
/// ```gleam
/// import dream_test/unit.{describe, it, before_all, after_all, to_test_suite}
/// import dream_test/runner.{run_suite}
/// import dream_test/types.{AssertionOk}
///
/// describe("External API integration", [
/// before_all(fn() {
/// start_mock_server(port: 8080)
/// AssertionOk
/// }),
///
/// it("fetches users", fn() { ... }),
/// it("creates users", fn() { ... }),
/// it("handles errors", fn() { ... }),
///
/// after_all(fn() {
/// // Clean up even if tests failed
/// stop_mock_server()
/// AssertionOk
/// }),
/// ])
/// |> to_test_suite("api_test")
/// |> run_suite()
/// ```
///
/// ## Complete Lifecycle Example
///
/// Here's a complete example showing all four hooks working together:
///
/// ```gleam
/// describe("Database tests", [
/// // Once at start: start the database
/// before_all(fn() { start_db(); AssertionOk }),
///
/// // Before each test: begin a transaction
/// before_each(fn() { begin_transaction(); AssertionOk }),
///
/// it("creates records", fn() { ... }),
/// it("queries records", fn() { ... }),
///
/// // After each test: rollback the transaction
/// after_each(fn() { rollback_transaction(); AssertionOk }),
///
/// // Once at end: stop the database
/// after_all(fn() { stop_db(); AssertionOk }),
/// ])
/// ```
///
/// ## Important: Requires Suite Mode
///
/// `after_all` hooks only work with `to_test_suite` + `run_suite`.
/// When using `to_test_cases` + `run_all`, they are silently ignored.
///
pub fn after_all(teardown: fn() -> AssertionResult) -> UnitTest {
AfterAll(teardown)
}
/// Context for tracking inherited hooks during tree traversal.
///
/// This is used internally when converting a test tree to test cases.
/// Hooks are inherited from parent describe blocks.
///
type HookContext {
HookContext(
/// before_each hooks in outer-to-inner order
before_each_hooks: List(fn() -> AssertionResult),
/// after_each hooks in inner-to-outer order
after_each_hooks: List(fn() -> AssertionResult),
)
}
fn empty_hook_context() -> HookContext {
HookContext(before_each_hooks: [], after_each_hooks: [])
}
/// Convert a test tree into a flat list of runnable test cases.
///
/// This function walks the `UnitTest` tree and produces `TestCase` values
/// that the runner can execute. Each test case includes:
///
/// - `name` - The test's own name (from `it`)
/// - `full_name` - The complete path including all `describe` ancestors
/// - `tags` - Currently empty (tag support coming soon)
/// - `kind` - Set to `Unit` for all tests from this DSL
/// - `before_each_hooks` - Inherited hooks to run before the test
/// - `after_each_hooks` - Inherited hooks to run after the test
///
/// ## Hook Handling
///
/// - `before_each`/`after_each` hooks are collected and attached to each test
/// - `before_all`/`after_all` hooks are ignored (use `to_test_suite` instead)
///
/// ## Example
///
/// ```gleam
/// let test_cases =
/// describe("Math", [
/// it("adds", fn() { ... }),
/// it("subtracts", fn() { ... }),
/// ])
/// |> to_test_cases("math_test")
///
/// // test_cases is now a List(TestCase) ready for run_all()
/// ```
///
/// ## Parameters
///
/// - `module_name` - The name of the test module (used for identification)
/// - `root` - The root `UnitTest` node (typically from `describe`)
///
pub fn to_test_cases(module_name: String, root: UnitTest) -> List(TestCase) {
let context = empty_hook_context()
to_test_cases_from_unit_test(module_name, [], context, root, [])
}
fn to_test_cases_from_unit_test(
module_name: String,
name_prefix: List(String),
hook_context: HookContext,
node: UnitTest,
accumulated: List(TestCase),
) -> List(TestCase) {
case node {
ItTest(name, run) ->
build_it_test_case(name_prefix, name, run, hook_context, accumulated)
DescribeGroup(name, children) -> {
let new_prefix = list.append(name_prefix, [name])
let group_hooks = collect_hooks_from_children(children, hook_context)
to_test_cases_from_list(
module_name,
new_prefix,
group_hooks,
children,
accumulated,
)
}
// before_all and after_all are ignored in flat mode
BeforeAll(_) -> accumulated
AfterAll(_) -> accumulated
// before_each and after_each at the root level (outside describe) are ignored
BeforeEach(_) -> accumulated
AfterEach(_) -> accumulated
}
}
/// Collect hooks from a list of children and merge with inherited hooks.
fn collect_hooks_from_children(
children: List(UnitTest),
inherited: HookContext,
) -> HookContext {
collect_hooks_from_list(children, inherited)
}
fn collect_hooks_from_list(
remaining: List(UnitTest),
context: HookContext,
) -> HookContext {
case remaining {
[] -> context
[head, ..tail] -> {
let updated = collect_hook_from_node(head, context)
collect_hooks_from_list(tail, updated)
}
}
}
fn collect_hook_from_node(node: UnitTest, context: HookContext) -> HookContext {
case node {
BeforeEach(setup) -> {
// Append to maintain outer-to-inner order
let hooks = list.append(context.before_each_hooks, [setup])
HookContext(..context, before_each_hooks: hooks)
}
AfterEach(teardown) -> {
// Prepend to maintain inner-to-outer order
HookContext(..context, after_each_hooks: [
teardown,
..context.after_each_hooks
])
}
// Other nodes don't affect hooks
_ -> context
}
}
fn build_it_test_case(
name_prefix: List(String),
name: String,
run: fn() -> AssertionResult,
hook_context: HookContext,
accumulated: List(TestCase),
) -> List(TestCase) {
let full_name = list.append(name_prefix, [name])
let config =
SingleTestConfig(
name: name,
full_name: full_name,
tags: [],
kind: Unit,
run: run,
timeout_ms: None,
before_each_hooks: hook_context.before_each_hooks,
after_each_hooks: hook_context.after_each_hooks,
)
let test_case = TestCase(config)
[test_case, ..accumulated]
}
fn to_test_cases_from_list(
module_name: String,
name_prefix: List(String),
hook_context: HookContext,
remaining: List(UnitTest),
accumulated: List(TestCase),
) -> List(TestCase) {
case remaining {
[] -> list.reverse(accumulated)
[head, ..tail] -> {
let updated =
to_test_cases_from_unit_test(
module_name,
name_prefix,
hook_context,
head,
accumulated,
)
to_test_cases_from_list(
module_name,
name_prefix,
hook_context,
tail,
updated,
)
}
}
}
// =============================================================================
// Test Suite Conversion (for before_all/after_all support)
// =============================================================================
/// Convert a test tree into a structured test suite.
///
/// Use `to_test_suite` when you need `before_all` or `after_all` hooks.
/// Unlike `to_test_cases`, this preserves the group hierarchy required
/// for once-per-group semantics.
///
/// ## When to Use Each Mode
///
/// | Scenario | Function | Runner |
/// |---------------------------------------------|------------------|--------------|
/// | Simple tests, no hooks | `to_test_cases` | `run_all` |
/// | Only `before_each`/`after_each` | `to_test_cases` | `run_all` |
/// | Need `before_all` or `after_all` | `to_test_suite` | `run_suite` |
/// | Expensive setup shared across tests | `to_test_suite` | `run_suite` |
///
/// ## How It Works
///
/// ```text
/// describe("A", [ TestSuite("A")
/// before_all(setup), → before_all: [setup]
/// it("test1", ...), items: [
/// describe("B", [ SuiteTest(test1),
/// it("test2", ...), SuiteGroup(TestSuite("B", ...))
/// ]), ]
/// ])
/// ```
///
/// The tree structure is preserved, allowing the runner to execute
/// `before_all` before entering a group and `after_all` after leaving.
///
/// ## Example
///
/// ```gleam
/// import dream_test/unit.{describe, it, before_all, after_all, to_test_suite}
/// import dream_test/runner.{run_suite}
/// import dream_test/reporter/bdd.{report}
/// import dream_test/types.{AssertionOk}
/// import gleam/io
///
/// pub fn main() {
/// tests()
/// |> to_test_suite("integration_test")
/// |> run_suite()
/// |> report(io.print)
/// }
///
/// pub fn tests() {
/// describe("Payment processing", [
/// before_all(fn() {
/// start_payment_gateway_mock()
/// AssertionOk
/// }),
///
/// describe("successful payments", [
/// it("processes credit cards", fn() { ... }),
/// it("processes debit cards", fn() { ... }),
/// ]),
///
/// describe("failed payments", [
/// it("handles declined cards", fn() { ... }),
/// it("handles network errors", fn() { ... }),
/// ]),
///
/// after_all(fn() {
/// stop_payment_gateway_mock()
/// AssertionOk
/// }),
/// ])
/// }
/// ```
///
/// ## Parameters
///
/// - `module_name` - Name of the test module (appears in output)
/// - `root` - The root `UnitTest` node (typically from `describe`)
///
/// ## Returns
///
/// A `TestSuite` that can be executed with `run_suite` or `run_suite_with_config`.
///
pub fn to_test_suite(module_name: String, root: UnitTest) -> TestSuite {
let context = empty_hook_context()
to_suite_from_unit_test(module_name, [], context, root)
}
fn to_suite_from_unit_test(
module_name: String,
name_prefix: List(String),
inherited_hooks: HookContext,
node: UnitTest,
) -> TestSuite {
case node {
DescribeGroup(name, children) -> {
let full_name = list.append(name_prefix, [name])
build_suite_from_describe(
module_name,
full_name,
inherited_hooks,
children,
)
}
// If root is not a describe, wrap it in a synthetic suite
ItTest(name, run) -> {
let full_name = list.append(name_prefix, [name])
let test_case =
build_single_test_case(full_name, name, run, inherited_hooks)
TestSuite(
name: module_name,
full_name: [module_name],
before_all_hooks: [],
after_all_hooks: [],
items: [SuiteTest(test_case)],
)
}
// Hooks at root level are ignored
BeforeAll(_) | BeforeEach(_) | AfterEach(_) | AfterAll(_) ->
TestSuite(
name: module_name,
full_name: [module_name],
before_all_hooks: [],
after_all_hooks: [],
items: [],
)
}
}
fn build_suite_from_describe(
module_name: String,
full_name: List(String),
inherited_hooks: HookContext,
children: List(UnitTest),
) -> TestSuite {
// Collect hooks from this level
let suite_hooks = collect_suite_hooks(children)
let group_hooks = collect_hooks_from_children(children, inherited_hooks)
// Build items from children
let items =
build_suite_items(module_name, full_name, group_hooks, children, [])
// Extract name from full_name
let name = extract_last_name(full_name)
TestSuite(
name: name,
full_name: full_name,
before_all_hooks: suite_hooks.before_all,
after_all_hooks: suite_hooks.after_all,
items: items,
)
}
/// Hooks specific to a suite (before_all/after_all)
type SuiteHooks {
SuiteHooks(
before_all: List(fn() -> AssertionResult),
after_all: List(fn() -> AssertionResult),
)
}
fn collect_suite_hooks(children: List(UnitTest)) -> SuiteHooks {
collect_suite_hooks_from_list(
children,
SuiteHooks(before_all: [], after_all: []),
)
}
fn collect_suite_hooks_from_list(
remaining: List(UnitTest),
hooks: SuiteHooks,
) -> SuiteHooks {
case remaining {
[] ->
SuiteHooks(
before_all: list.reverse(hooks.before_all),
after_all: list.reverse(hooks.after_all),
)
[head, ..tail] -> {
let updated = collect_suite_hook(head, hooks)
collect_suite_hooks_from_list(tail, updated)
}
}
}
fn collect_suite_hook(node: UnitTest, hooks: SuiteHooks) -> SuiteHooks {
case node {
BeforeAll(setup) ->
SuiteHooks(..hooks, before_all: [setup, ..hooks.before_all])
AfterAll(teardown) ->
SuiteHooks(..hooks, after_all: [teardown, ..hooks.after_all])
_ -> hooks
}
}
fn build_suite_items(
module_name: String,
name_prefix: List(String),
hook_context: HookContext,
remaining: List(UnitTest),
accumulated: List(TestSuiteItem),
) -> List(TestSuiteItem) {
case remaining {
[] -> list.reverse(accumulated)
[head, ..tail] -> {
let new_items =
build_suite_item(module_name, name_prefix, hook_context, head)
let updated = list.append(list.reverse(new_items), accumulated)
build_suite_items(module_name, name_prefix, hook_context, tail, updated)
}
}
}
fn build_suite_item(
module_name: String,
name_prefix: List(String),
hook_context: HookContext,
node: UnitTest,
) -> List(TestSuiteItem) {
case node {
ItTest(name, run) -> {
let full_name = list.append(name_prefix, [name])
let test_case = build_single_test_case(full_name, name, run, hook_context)
[SuiteTest(test_case)]
}
DescribeGroup(name, children) -> {
let full_name = list.append(name_prefix, [name])
let nested_suite =
build_suite_from_describe(
module_name,
full_name,
hook_context,
children,
)
[SuiteGroup(nested_suite)]
}
// Hooks are collected separately, not items
BeforeAll(_) | BeforeEach(_) | AfterEach(_) | AfterAll(_) -> []
}
}
fn build_single_test_case(
full_name: List(String),
name: String,
run: fn() -> AssertionResult,
hook_context: HookContext,
) -> TestCase {
let config =
SingleTestConfig(
name: name,
full_name: full_name,
tags: [],
kind: Unit,
run: run,
timeout_ms: None,
before_each_hooks: hook_context.before_each_hooks,
after_each_hooks: hook_context.after_each_hooks,
)
TestCase(config)
}
fn extract_last_name(full_name: List(String)) -> String {
case list.reverse(full_name) {
[last, ..] -> last
[] -> ""
}
}