dream_test/process

Process helpers for tests that need actors or async operations.

When tests run in isolated BEAM processes, any processes spawned by the test are automatically terminated when the test ends. This module provides helpers for common patterns.

Auto-Cleanup

Processes started with these helpers are linked to the test process. When the test completes (pass, fail, or timeout), all linked processes are automatically cleaned up. No manual teardown needed.

Quick Start

import dream_test/process.{start_counter, get_count, increment}
import dream_test/unit.{describe, it}
import dream_test/assertions/should.{should, equal, or_fail_with}

pub fn tests() {
  describe("Counter", [
    it("increments correctly", fn() {
      let counter = start_counter()
      increment(counter)
      increment(counter)

      get_count(counter)
      |> should()
      |> equal(2)
      |> or_fail_with("Counter should be 2")
    }),
  ])
}

Available Helpers

HelperPurpose
start_counterSimple counter actor for testing state
start_actorGeneric actor with custom state and handler
unique_portGenerate random port for test servers
await_readyPoll until a condition is true
await_somePoll until a function returns Ok

Types

Messages for the built-in counter actor.

Use these with the counter functions or send them directly:

let counter = start_counter()
process.send(counter, Increment)
process.send(counter, SetCount(100))
pub type CounterMessage {
  Increment
  Decrement
  SetCount(Int)
  GetCount(process.Subject(Int))
}

Constructors

Configuration for polling operations.

Controls how long to wait and how often to check.

Fields

  • timeout_ms - Maximum time to wait before giving up
  • interval_ms - How often to check the condition

Example

// Check every 100ms for up to 10 seconds
PollConfig(timeout_ms: 10_000, interval_ms: 100)
pub type PollConfig {
  PollConfig(timeout_ms: Int, interval_ms: Int)
}

Constructors

  • PollConfig(timeout_ms: Int, interval_ms: Int)

    Arguments

    timeout_ms

    Maximum time to wait in milliseconds.

    interval_ms

    How often to check the condition in milliseconds.

Result of a polling operation.

Either the condition was met (Ready) or we gave up (TimedOut).

pub type PollResult(a) {
  Ready(a)
  TimedOut
}

Constructors

  • Ready(a)

    The condition was met and returned this value.

  • TimedOut

    Timed out waiting for the condition.

Port selection strategies for test servers.

When starting test servers, you need to choose a port:

  • Port(n) - Use a specific port number
  • RandomPort - Pick a random available port (recommended)
pub type PortSelection {
  Port(Int)
  RandomPort
}

Constructors

  • Port(Int)

    Use a specific port number.

  • RandomPort

    Pick a random available port in a safe range (10000-60000).

Values

pub fn await_ready(
  config: PollConfig,
  check: fn() -> Bool,
) -> PollResult(Bool)

Wait until a condition returns True.

Polls the check function at regular intervals until it returns True or the timeout is reached.

Use Cases

  • Waiting for a server to start accepting connections
  • Waiting for a file to appear
  • Waiting for a service to become healthy

Example

// Wait for server to be ready
case await_ready(quick_poll_config(), fn() { is_port_open(port) }) {
  Ready(True) -> {
    // Server is up, proceed with test
    make_request(port)
    |> should()
    |> be_ok()
    |> or_fail_with("Request should succeed")
  }
  TimedOut -> fail_with("Server didn't start in time")
}
pub fn await_some(
  config: PollConfig,
  check: fn() -> Result(a, e),
) -> PollResult(a)

Wait until a function returns Ok.

Polls the check function at regular intervals until it returns Ok(value) or the timeout is reached. The value is returned in Ready(value).

Use Cases

  • Waiting for a record to appear in a database
  • Waiting for an async job to complete
  • Waiting for a resource to become available

Example

// Wait for user to appear in database
case await_some(default_poll_config(), fn() { find_user(user_id) }) {
  Ready(user) -> {
    user.name
    |> should()
    |> equal("Alice")
    |> or_fail_with("User should be Alice")
  }
  TimedOut -> fail_with("User never appeared in database")
}
pub fn call_actor(
  subject: process.Subject(msg),
  make_message: fn(process.Subject(reply)) -> msg,
  timeout_ms: Int,
) -> reply

Call an actor and wait for a response.

This is a convenience wrapper around actor.call that makes the parameter order more ergonomic for piping.

Parameters

  • subject - The actor to call
  • make_message - Function that creates the message given a reply subject
  • timeout_ms - How long to wait for a response

Example

pub type Msg {
  GetValue(Subject(Int))
}

let value = call_actor(my_actor, GetValue, 1000)
pub fn decrement(counter: process.Subject(CounterMessage)) -> Nil

Decrement a counter by 1.

This is an asynchronous send—it returns immediately.

Example

let counter = start_counter_with(10)
decrement(counter)
get_count(counter)  // -> 9
pub fn default_poll_config() -> PollConfig

Default polling configuration.

  • 5 second timeout
  • Check every 50ms

Good for operations that might take a few seconds.

pub fn get_count(counter: process.Subject(CounterMessage)) -> Int

Get the current value from a counter.

This is a synchronous call that blocks until the counter responds.

Example

let counter = start_counter()
increment(counter)
let value = get_count(counter)  // -> 1
pub fn increment(counter: process.Subject(CounterMessage)) -> Nil

Increment a counter by 1.

This is an asynchronous send—it returns immediately.

Example

let counter = start_counter()
increment(counter)
increment(counter)
get_count(counter)  // -> 2
pub fn quick_poll_config() -> PollConfig

Quick polling configuration.

  • 1 second timeout
  • Check every 10ms

Good for fast local operations like servers starting.

pub fn set_count(
  counter: process.Subject(CounterMessage),
  value: Int,
) -> Nil

Set a counter to a specific value.

This is an asynchronous send—it returns immediately.

Example

let counter = start_counter()
set_count(counter, 42)
get_count(counter)  // -> 42
pub fn start_actor(
  initial_state: state,
  handler: fn(state, msg) -> actor.Next(state, msg),
) -> process.Subject(msg)

Start a generic actor with custom state and message handler.

The actor is linked to the test process and will be automatically cleaned up when the test ends.

Parameters

  • initial_state - The actor’s starting state
  • handler - Function fn(state, message) -> actor.Next(state, message)

Example

pub type TodoMessage {
  Add(String)
  GetAll(Subject(List(String)))
}

let todos = start_actor([], fn(items, msg) {
  case msg {
    Add(item) -> actor.continue([item, ..items])
    GetAll(reply) -> {
      process.send(reply, items)
      actor.continue(items)
    }
  }
})

process.send(todos, Add("Write tests"))
process.send(todos, Add("Run tests"))
let items = call_actor(todos, GetAll, 1000)
// items == ["Run tests", "Write tests"]
pub fn start_counter() -> process.Subject(CounterMessage)

Start a counter actor initialized to 0.

The counter is linked to the test process and will be automatically cleaned up when the test ends.

Example

let counter = start_counter()
increment(counter)
increment(counter)
get_count(counter)  // -> 2
pub fn start_counter_with(
  initial: Int,
) -> process.Subject(CounterMessage)

Start a counter actor with a specific initial value.

Example

let counter = start_counter_with(100)
decrement(counter)
get_count(counter)  // -> 99
pub fn unique_port() -> Int

Generate a unique port number for test servers.

Returns a random port between 10,000 and 60,000. This range avoids:

  • Well-known ports (0-1023)
  • Registered ports commonly used by services (1024-9999)
  • Ports near the upper limit that some systems reserve

Using random ports prevents conflicts when running tests in parallel.

Example

let port = unique_port()
start_server(port)
// port is something like 34521
Search Document