Version: 0.6.0 Last Updated: March 3, 2026
Overview
Supertester is a deterministic OTP testing toolkit for Elixir. It focuses on:
- async-safe test isolation
- cast/call synchronization without timing sleeps
- supervisor strategy and tree verification
- chaos and performance diagnostics
- OTP-aware assertions
Installation
Add to your mix.exs:
def deps do
[
{:supertester, "~> 0.6.0", only: :test}
]
endThen run mix deps.get.
Prerequisites
TestableGenServer
Most Supertester workflows revolve around cast_and_sync/4, which sends a cast and then makes a synchronizing call to ensure the cast was processed. For this to work, your GenServer must handle the sync message.
Add use Supertester.TestableGenServer after use GenServer:
defmodule MyApp.Counter do
use GenServer
use Supertester.TestableGenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, %{count: 0}, opts)
@impl true
def init(state), do: {:ok, state}
@impl true
def handle_cast(:increment, state) do
{:noreply, %{state | count: state.count + 1}}
end
endThis injects a handle_call(:__supertester_sync__, ...) clause that replies :ok. It also supports {:__supertester_sync__, return_state: true} which replies with {:ok, state}.
Recommended Setup
Use the ExUnit adapter in test modules:
defmodule MyApp.SomeTest do
use Supertester.ExUnitFoundation, isolation: :full_isolation
endIsolation modes:
:basic— minimal tracking, no registry:registry— shared Registry for process naming:full_isolation— full process/ETS tracking with cleanup:contamination_detection— compares before/after state (runs sync)
Optional adapter features:
telemetry_isolation: truelogger_isolation: trueets_isolation: [:named_table, ...]- tags:
@tag telemetry_events: [...],@tag logger_level: :debug,@tag ets_tables: [...]
Core Workflow
1) Start isolated OTP subjects
import Supertester.OTPHelpers
{:ok, server} = setup_isolated_genserver(MyServer)
{:ok, sup} = setup_isolated_supervisor(MySupervisor)These helpers generate unique process names (using {:via, Registry, ...} tuples) so concurrent async tests never collide. Processes are tracked and cleaned up automatically when the test exits.
2) Replace sleeps with synchronization
import Supertester.GenServerHelpers
:ok = cast_and_sync(server, :do_work)cast_and_sync/4 semantics:
- When the sync handler replies
:ok(the defaultTestableGenServerbehavior), returns bare:ok. - When the sync handler replies with any other value, returns
{:ok, reply}. - strict mode (
strict?: true) raisesArgumentErrorif sync handling is missing. - non-strict mode returns
{:error, :missing_sync_handler}if missing.
3) Assert OTP behavior
import Supertester.Assertions
assert_genserver_state(server, fn s -> s.ready end)
assert_genserver_responsive(server)
assert_all_children_alive(sup)4) Verify supervisor strategies
import Supertester.SupervisorHelpers
result = test_restart_strategy(sup, :one_for_one, {:kill_child, :worker_1})
assert :worker_1 in result.restartedBehavior:
- validates expected strategy at runtime and raises on mismatch.
- raises when scenario child IDs are missing.
- removed temporary children are not reported as restarted.
5) Add resilience testing
import Supertester.ChaosHelpers
report = chaos_kill_children(sup, kill_rate: 0.4, duration_ms: 1_000)Behavior:
- accepts pid and registered names (atom,
{:global, _},{:via, _, _}). restartedtracks observed child replacements, including cascade replacements.- returns a report even if supervisor dies (
supervisor_crashed: true).
6) Run suites with deadlines
report = run_chaos_suite(sup, scenarios, timeout: 5_000)Behavior:
- enforces a suite-level deadline.
- timed-out execution is reported as
:timeoutand remaining scenarios as:suite_timeout. - scenario results that happen to be
{:error, :timeout}are treated as ordinary failures, not suite cutoffs.
7) Leak and performance checks
import Supertester.{Assertions, PerformanceHelpers}
assert_no_process_leaks(fn ->
{:ok, pid} = Agent.start_link(fn -> :ok end)
Agent.stop(pid)
end)
assert_performance(fn -> heavy_call() end, max_time_ms: 100)Leak behavior:
- attributes leaks to spawned/linked process trees from the operation.
- catches delayed descendants.
- ignores short-lived transient processes.
- propagates exceptions from the operation (no false leak reports on raise).
Custom Harness Integration (Supertester.Env)
Supertester delegates cleanup registration to Supertester.Env, which defaults to ExUnit.Callbacks.on_exit/1. To integrate with a custom test runner:
- Implement the
Supertester.Envbehaviour:
defmodule MyHarness.TestEnv do
@behaviour Supertester.Env
@impl true
def on_exit(callback) when is_function(callback, 0) do
MyHarness.register_cleanup(callback)
end
end- Configure it in your test config:
# config/test.exs
config :supertester, env_module: MyHarness.TestEnvThe on_exit/1 callback receives a zero-arity function that must be called when the test finishes to clean up isolated processes, ETS tables, and other resources.
Safety Notes
- Library code avoids unbounded dynamic atom creation for test IDs and isolation naming.
- Process names use
{:via, Registry, ...}tuples instead of dynamic atoms. - ETS fallback injection requires existing env keys and refuses dynamic atom creation.
Common Patterns
Deterministic GenServer test
defmodule MyApp.CounterTest do
use Supertester.ExUnitFoundation, isolation: :full_isolation
import Supertester.{OTPHelpers, GenServerHelpers, Assertions}
test "increments deterministically" do
{:ok, counter} = setup_isolated_genserver(Counter)
:ok = cast_and_sync(counter, :increment)
assert_genserver_state(counter, fn state -> state.count == 1 end)
end
endVerify supervisor tree structure
assert_supervision_tree_structure(sup, %{
supervisor: MySupervisor,
strategy: :one_for_one,
children: [
{:worker_1, MyWorker},
{:worker_2, MyWorker}
]
})Chaos resilience assertion
assert_chaos_resilient(
sup,
fn -> chaos_kill_children(sup, kill_rate: 0.5, duration_ms: 200) end,
fn -> length(Supervisor.which_children(sup)) == expected_count end,
timeout: 2_000
)Troubleshooting
"no handle_call/3 clause was provided" or missing sync handler errors
Your GenServer does not handle :__supertester_sync__. Add use Supertester.TestableGenServer to your module.
Tests fail with {:error, :noproc}
The GenServer has crashed or been stopped before your assertion. Check for unhandled messages or invalid state transitions.
Tests pass individually but fail when run together
You may be using named processes without isolation. Use setup_isolated_genserver/3 which generates unique names, or use Supertester.ExUnitFoundation with :full_isolation.
{:error, :missing_sync_handler} from cast_and_sync
The server is missing a sync handler. Either add use Supertester.TestableGenServer to the server module, or implement a handle_call(:__supertester_sync__, ...) clause manually.
Public Modules
SupertesterSupertester.ExUnitFoundationSupertester.UnifiedTestFoundationSupertester.TestableGenServerSupertester.EnvSupertester.OTPHelpersSupertester.GenServerHelpersSupertester.SupervisorHelpersSupertester.ChaosHelpersSupertester.PerformanceHelpersSupertester.AssertionsSupertester.ConcurrentHarnessSupertester.PropertyHelpersSupertester.MessageHarnessSupertester.TelemetrySupertester.TelemetryHelpersSupertester.LoggerIsolationSupertester.ETSIsolation