Supertester
View Source
A battle-hardened testing toolkit for building robust and resilient Elixir & OTP applications.
The Problem: Flaky OTP Tests
Are you tired of...
- 😫 Flaky tests that fail randomly due to race conditions?
- 📛
GenServername clashes when running tests withasync: true? - 🕰️ Littering your test suite with
Process.sleep/1and hoping for the best? - 🤷♂️ Struggling to test process crashes, restarts, and complex supervision trees?
Writing tests for concurrent systems is hard. Traditional testing methods often lead to fragile, non-deterministic, and slow test suites.
The Solution: Supertester
Supertester provides a comprehensive suite of tools to write clean, deterministic, and reliable tests for your OTP applications. It replaces fragile timing hacks with robust synchronization patterns and provides powerful helpers for simulating and asserting complex OTP behaviors.
With Supertester, you can build a test suite that is fast, parallel, and trustworthy.
Key Features
- ✅ Rock-Solid Test Isolation: Run all your tests with
async: truewithout fear of process name collisions or state leakage. - 🔄 Deterministic Synchronization: No more
Process.sleep/1. Use helpers that wait for processes to be ready,casts to be processed, and supervisors to stabilize. - 🤖 Powerful OTP Assertions: Go beyond
assert. Useassert_process_restarted/2,assert_genserver_state/2, andassert_all_children_alive/1for more expressive tests. - ✨ Effortless Setup & Teardown: Start isolated
GenServers andSupervisors with a single line and trustSupertesterto handle all the cleanup. - 💥 Resilience & Chaos Testing: Tools for testing crash recovery, supervision strategies, and system stability under stress.
Installation
Add supertester as a dependency in your mix.exs file. It's only needed for the :test environment.
def deps do
[
{:supertester, "~> 0.1.0", only: :test}
]
endThen, run mix deps.get to install.
Quick Start: From Flaky to Robust
See how Supertester transforms a common, fragile test pattern into a robust, deterministic one.
Before: The Flaky Way
# test/my_app/counter_test.exs
defmodule MyApp.CounterTest do
use ExUnit.Case, async: false # <-- Forced to run sequentially
test "incrementing the counter" do
# Manual setup, prone to name conflicts
{:ok, _pid} = start_supervised({Counter, name: Counter})
GenServer.cast(Counter, :increment)
Process.sleep(50) # <-- Fragile, timing-dependent guess
state = GenServer.call(Counter, :state)
assert state.count == 1
end
endAfter: The Supertester Way
# test/my_app/counter_test.exs
defmodule MyApp.CounterTest do
use ExUnit.Case, async: true # <-- Fully parallel!
# Import the tools you need
import Supertester.OTPHelpers
import Supertester.GenServerHelpers
import Supertester.Assertions
test "incrementing the counter" do
# Isolated setup with automatic cleanup, no name clashes
{:ok, counter_pid} = setup_isolated_genserver(Counter)
# Deterministic sync: ensures the cast is processed before continuing
:ok = cast_and_sync(counter_pid, :increment)
# Expressive, OTP-aware assertion for checking state
assert_genserver_state(counter_pid, fn state -> state.count == 1 end)
end
endCore API Highlights
Supertester is organized into several modules, each targeting a specific area of OTP testing.
Supertester.OTPHelpers
For setting up and managing isolated OTP processes.
setup_isolated_genserver/3: Starts aGenServerwith a unique name and automatic cleanup.setup_isolated_supervisor/3: Starts aSupervisorwith a unique name and automatic cleanup.wait_for_process_restart/3: Blocks until a supervised process has been terminated and restarted.wait_for_genserver_sync/2: Ensures aGenServeris alive and responsive.
Supertester.GenServerHelpers
For interacting with and testing GenServers.
cast_and_sync/3: Sends acastand waits for a follow-upcallto confirm it was processed.get_server_state_safely/1: FetchesGenServerstate without crashing if the process is down.test_server_crash_recovery/2: Simulates a process crash and verifies its recovery by the supervisor.concurrent_calls/3: Stress-tests aGenServerwith many concurrent requests.
Supertester.Assertions
Custom, OTP-aware assertions for more meaningful tests.
assert_process_alive/1&assert_process_dead/1assert_genserver_state/2: Asserts theGenServer's internal state matches a value or passes a function check.assert_child_count/2: Asserts a supervisor has an exact number of active children.assert_all_children_alive/1: Checks that all children in a supervision tree are running.assert_no_process_leaks/1: Ensures an operation cleans up all the processes it spawns.
Supertester.UnifiedTestFoundation
Provides advanced, case-level isolation for complex scenarios.
defmodule MyApp.MyAdvancedTest do
use ExUnit.Case
# Choose an isolation level for the entire test module.
# :full_isolation provides sandboxed processes and ETS tables.
use Supertester.UnifiedTestFoundation, isolation: :full_isolation
test "this test runs in a complete sandbox", context do
# `context.isolation_context` holds info about the sandbox.
# All processes started via Supertester helpers are tracked and auto-cleaned.
{:ok, server} = Supertester.OTPHelpers.setup_isolated_genserver(MyServer)
# ... your isolated test logic ...
end
endContributing
Contributions are welcome! If you'd like to help improve Supertester, please feel free to:
- Fork the repository.
- Create a new feature branch.
- Add your feature or bug fix.
- Ensure all new code is covered by tests.
- Open a pull request.
License
This project is licensed under the MIT License.