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
| Helper | Purpose |
|---|---|
start_counter | Simple counter actor for testing state |
start_actor | Generic actor with custom state and handler |
unique_port | Generate random port for test servers |
await_ready | Poll until a condition is true |
await_some | Poll 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
-
Increment -
Decrement -
SetCount(Int) -
GetCount(process.Subject(Int))
Configuration for polling operations.
Controls how long to wait and how often to check.
Fields
timeout_ms- Maximum time to wait before giving upinterval_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.
-
TimedOutTimed 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 numberRandomPort- Pick a random available port (recommended)
pub type PortSelection {
Port(Int)
RandomPort
}
Constructors
-
Port(Int)Use a specific port number.
-
RandomPortPick 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 callmake_message- Function that creates the message given a reply subjecttimeout_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 statehandler- Functionfn(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