dream_test/process
Process helpers for tests that need actors or async operations.
When Dream Test runs a test, it runs in an isolated BEAM process. Any processes you spawn inside a test can be linked to that test process, so they automatically die when the test ends (pass, fail, timeout, crash).
This module gives you a few “batteries included” patterns:
- A simple counter actor (
start_counter) you can use to test stateful code. - A generic actor starter (
start_actor) + a pipe-friendly call helper (call_actor). - “wait until ready” polling (
await_ready/await_some) for async systems. - A safe-ish random port helper (
unique_port) for test servers.
Example
Use this inside an it block.
let counter = process.start_counter()
process.increment(counter)
process.increment(counter)
process.get_count(counter)
|> should
|> be_equal(2)
|> or_fail_with("expected counter to be 2")
Types
Messages for the built-in counter actor.
Most of the time you’ll use the helpers (increment, set_count, etc), but
you can also send the messages directly when it’s convenient.
Example
Use this inside an it block.
let counter = process.start_counter()
erlang_process.send(counter, process.Increment)
erlang_process.send(counter, process.SetCount(10))
process.get_count(counter)
|> should
|> be_equal(10)
|> or_fail_with("expected counter to be 10 after SetCount")
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
process.default_poll_config()
|> should
|> be_equal(process.PollConfig(timeout_ms: 5000, interval_ms: 50))
|> or_fail_with("expected default_poll_config to be 5000ms/50ms")
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).
Constructors
Ready(value): The condition was met.TimedOut: The timeout elapsed.
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)
Example
Use this anywhere you need a port selection value.
process.Port(1234)
|> should
|> be_equal(process.Port(1234))
|> or_fail_with("expected PortSelection to be constructible")
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 config: PollConfig,
check 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
Parameters
config: Poll timeout + interval.check: A zero-arg function returningBool.
Returns
Ready(True) when the check returns True, otherwise TimedOut.
Example
process.await_ready(process.quick_poll_config(), always_true)
|> should
|> be_equal(process.Ready(True))
|> or_fail_with("expected await_ready to return Ready(True)")
pub fn await_some(
config config: PollConfig,
check 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
Parameters
config: Poll timeout + interval.check: A zero-arg function returningResult(value, error).
Returns
Ready(value) when the check returns Ok(value), otherwise TimedOut.
Example
process.await_some(process.default_poll_config(), always_ok_42)
|> should
|> be_equal(process.Ready(42))
|> or_fail_with("expected await_some to return Ready(42)")
pub fn call_actor(
subject subject: process.Subject(msg),
make_message make_message: fn(process.Subject(reply)) -> msg,
timeout_ms 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
Returns
The reply value from the actor.
Example
process.call_actor(todos, GetAll, 1000)
|> should
|> be_equal(["Write tests", "Run tests"])
|> or_fail_with("expected items to be preserved in insertion order")
pub fn decrement(
counter counter: process.Subject(CounterMessage),
) -> Nil
Decrement a counter by 1.
This is an asynchronous send—it returns immediately.
Parameters
counter: The counter actor to decrement.
Returns
Nil. (The message is sent asynchronously.)
Example
Use this inside an it block.
let counter = process.start_counter_with(10)
process.decrement(counter)
process.get_count(counter)
|> should
|> be_equal(9)
|> or_fail_with("expected counter to be 9 after decrement")
pub fn default_poll_config() -> PollConfig
Default polling configuration.
- 5 second timeout
- Check every 50ms
Good for operations that might take a few seconds.
Returns
PollConfig(timeout_ms: 5000, interval_ms: 50).
Example
process.default_poll_config()
|> should
|> be_equal(process.PollConfig(timeout_ms: 5000, interval_ms: 50))
|> or_fail_with("expected default_poll_config to be 5000ms/50ms")
pub fn get_count(
counter counter: process.Subject(CounterMessage),
) -> Int
Get the current value from a counter.
This is a synchronous call that blocks until the counter responds.
Parameters
counter: The counter actor to query.
Returns
The current counter value.
Example
Use this inside an it block.
let counter = process.start_counter()
process.increment(counter)
process.get_count(counter)
|> should
|> be_equal(1)
|> or_fail_with("expected counter to be 1")
pub fn increment(
counter counter: process.Subject(CounterMessage),
) -> Nil
Increment a counter by 1.
This is an asynchronous send—it returns immediately.
Parameters
counter: The counter actor to increment.
Returns
Nil. (The message is sent asynchronously.)
Example
Use this inside an it block.
let counter = process.start_counter()
process.increment(counter)
process.increment(counter)
process.get_count(counter)
|> should
|> be_equal(2)
|> or_fail_with("expected counter to be 2")
pub fn quick_poll_config() -> PollConfig
Quick polling configuration.
- 1 second timeout
- Check every 10ms
Good for fast local operations like servers starting.
Returns
PollConfig(timeout_ms: 1000, interval_ms: 10).
Example
process.quick_poll_config()
|> should
|> be_equal(process.PollConfig(timeout_ms: 1000, interval_ms: 10))
|> or_fail_with("expected quick_poll_config to be 1000ms/10ms")
pub fn set_count(
counter counter: process.Subject(CounterMessage),
value value: Int,
) -> Nil
Set a counter to a specific value.
This is an asynchronous send—it returns immediately.
Parameters
counter: The counter actor to set.value: The new value to set.
Returns
Nil. (The message is sent asynchronously.)
Example
Use this inside an it block.
let counter = process.start_counter()
process.set_count(counter, 42)
process.get_count(counter)
|> should
|> be_equal(42)
|> or_fail_with("expected counter to be 42 after set_count")
pub fn start_actor(
initial_state initial_state: state,
handler 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)
Returns
A Subject(msg) you can send messages to (or call with call_actor).
Example
let todos = process.start_actor([], handle_todo_message)
erlang_process.send(todos, Add("Write tests"))
erlang_process.send(todos, Add("Run tests"))
process.call_actor(todos, GetAll, 1000)
|> should
|> be_equal(["Write tests", "Run tests"])
|> or_fail_with("expected items to be preserved in insertion order")
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.
Returns
A Subject(CounterMessage) you can pass to the other counter helpers (or
send messages to directly).
Example
Use this inside an it block.
let counter = process.start_counter()
process.increment(counter)
process.increment(counter)
process.get_count(counter)
|> should
|> be_equal(2)
|> or_fail_with("expected counter to be 2")
pub fn start_counter_with(
initial initial: Int,
) -> process.Subject(CounterMessage)
Start a counter actor with a specific initial value.
Parameters
initial: The initial count value.
Returns
A Subject(CounterMessage) for the started counter.
Example
Use this inside an it block.
let counter = process.start_counter_with(10)
process.decrement(counter)
process.get_count(counter)
|> should
|> be_equal(9)
|> or_fail_with("expected counter to be 9 after decrement")
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
Use this in test setup, before starting a server.
process.unique_port()
|> should
|> be_between(10_000, 60_000)
|> or_fail_with("expected unique_port to be within 10k..60k")