Supertester User Manual

View Source

Version: 0.5.1

Welcome to the comprehensive user manual for Supertester. This document provides a detailed overview of all modules and functions available in the Supertester toolkit.

Table of Contents

  1. Introduction
  2. Core Concepts
  3. Installation
  4. Core Modules
  5. OTP Testing Helpers
  6. Chaos Engineering
  7. Performance Testing
  8. Concurrency Harness
  9. Telemetry & Diagnostics
  10. Custom Assertions
  11. Practical Examples & Recipes
  12. Best Practices
  13. Troubleshooting

Introduction

Supertester is a battle-hardened OTP testing toolkit designed to help you build robust and reliable Elixir applications. It provides a comprehensive suite of tools for testing concurrent systems, including features for chaos engineering, performance testing, and zero-sleep synchronization.

This manual will guide you through the features and best practices for using Supertester to its full potential.

Core Concepts

Test Isolation

Supertester provides robust test isolation, allowing you to run your tests concurrently (async: true) without worrying about process name collisions or state leakage. This is achieved through the Supertester.UnifiedTestFoundation runtime and its ExUnit adapter, Supertester.ExUnitFoundation, which create a sandboxed environment for each test.

Zero Process.sleep

Timing-based synchronization (Process.sleep/1) is a common source of flaky tests. Supertester eliminates the need for this by providing deterministic synchronization patterns, such as cast_and_sync/3, which ensures that an asynchronous operation has completed before the test proceeds.

Automatic Cleanup

All resources created using Supertester's helpers (e.g., setup_isolated_genserver/3) are automatically cleaned up at the end of each test. This prevents resource leaks and ensures that tests do not interfere with each other.

Expressive Assertions

Supertester includes a rich set of OTP-aware assertions that make your tests more expressive and easier to read. For example, assert_genserver_state/2 allows you to assert on the internal state of a GenServer without manually fetching it.

Installation

To get started with Supertester, add it as a dependency in your mix.exs file. It's only required for the :test environment.

def deps do
  [
    {:supertester, "~> 0.5.1", only: :test}
  ]
end

Then, run mix deps.get to install the dependency.


Core Modules

Supertester

The main module provides basic information about the library.

version()

Returns the current version of the Supertester library.

  • Signature: @spec version() :: String.t()
  • Example:
    Supertester.version()
    #=> "0.5.1"

Supertester.ExUnitFoundation

This module is the drop-in ExUnit adapter for Supertester isolation. Replace use ExUnit.Case with it to automatically configure the appropriate async setting and install the isolation setup callback.

Usage:

defmodule MyApp.MyTest do
  use Supertester.ExUnitFoundation, isolation: :full_isolation

  test "an isolated test", context do
    # `context.isolation_context` contains isolation information.
    # All processes started with Supertester helpers are tracked and cleaned up.
  end
end

Isolation Modes (:isolation option):

  • :basic: Provides basic isolation with unique process naming (async-friendly).
  • :registry: Uses a dedicated registry for process isolation (async-friendly).
  • :full_isolation: Provides complete process and ETS table isolation. This is the recommended mode (async-friendly).
  • :contamination_detection: Detects if a test leaks processes or ETS tables (runs synchronously).

Additional Isolation Options:

  • telemetry_isolation: true enables Supertester.TelemetryHelpers.
  • logger_isolation: true enables Supertester.LoggerIsolation.
  • ets_isolation: [...] mirrors named ETS tables into isolated copies.
  • @tag telemetry_events: [...] auto-attaches isolated telemetry handlers.
  • @tag ets_tables: [...] mirrors tables for a single test.
  • @tag logger_level: :debug overrides the process log level for the test.
defmodule MyApp.MyTest do
  use Supertester.ExUnitFoundation,
    isolation: :full_isolation,
    telemetry_isolation: true,
    logger_isolation: true,
    ets_isolation: [:my_table]

  @tag telemetry_events: [[:supertester, :concurrent, :scenario, :stop]]
  @tag logger_level: :debug
  test "isolation extensions", _context do
    # ...
  end
end

Supertester.UnifiedTestFoundation

Supertester.UnifiedTestFoundation now focuses on the isolation runtime itself. Use it directly when integrating Supertester with a custom harness or non-ExUnit environment. The legacy use Supertester.UnifiedTestFoundation macro is still available but emits a compile-time warning—prefer Supertester.ExUnitFoundation for ExUnit integration.

Manual usage example:

defmodule CustomHarnessTest do
  use ExUnit.Case, async: true

  setup context do
    Supertester.UnifiedTestFoundation.setup_isolation(:full_isolation, context)
  end

  test "custom integration", context do
    assert match?(%Supertester.IsolationContext{}, context.isolation_context)
  end
end

The value stored in context.isolation_context is a %Supertester.IsolationContext{} struct that captures the test_id, tracked processes, ETS tables, and contextual tags (module, test name, isolation mode, etc.), making it easy to log or inspect diagnostics.

Supertester.Env

Supertester.Env abstracts how Supertester registers cleanup callbacks. By default it delegates to ExUnit.Callbacks.on_exit/1, but you can plug in a custom module (that implements the Supertester.Env behaviour) for other harnesses:

defmodule MyHarness.Env do
  @behaviour Supertester.Env

  @impl true
  def on_exit(fun) do
    MyHarness.register_cleanup(fun)
  end
end

# config/test.exs
import Config
config :supertester, :env_module, MyHarness.Env

Supertester.TestableGenServer

This behavior injects a __supertester_sync__ handler into your GenServers, enabling deterministic testing of asynchronous operations without Process.sleep/1.

Usage in your GenServer:

defmodule MyApp.MyServer do
  use GenServer
  use Supertester.TestableGenServer

  # Your GenServer implementation...
end

Usage in your tests:

test "testing an async operation" do
  {:ok, server} = MyApp.MyServer.start_link()

  # Perform an async operation
  GenServer.cast(server, :some_async_work)

  # Wait for the operation to complete
  :ok = GenServer.call(server, :__supertester_sync__)

  # Now it's safe to assert the state
  assert_genserver_state(server, fn state -> state.work_done == true end)
end

The injected handler also supports returning the state directly:

{:ok, state} = GenServer.call(server, {:__supertester_sync__, return_state: true})

OTP Testing Helpers

Supertester.OTPHelpers

This module contains core helpers for testing OTP-compliant processes.

setup_isolated_genserver(module, test_name \\ "", opts \\ [])

Starts an isolated GenServer with a unique name and automatic cleanup.

  • Signature: @spec setup_isolated_genserver(module(), String.t(), keyword()) :: {:ok, pid()} | {:error, term()}

  • Parameters:
    • module: The GenServer module to start.
    • test_name (optional): A name for the test context to ensure unique process naming.
    • opts (optional): Options to pass to GenServer.start_link/3.
  • Example:
    {:ok, server} = setup_isolated_genserver(MyServer, "my_test", [initial_state: %{}])

setup_isolated_supervisor(module, test_name \\ "", opts \\ [])

Starts an isolated Supervisor with a unique name and automatic cleanup.

  • Signature: @spec setup_isolated_supervisor(module(), String.t(), keyword()) :: {:ok, pid()} | {:error, term()}

  • Example:
    {:ok, supervisor} = setup_isolated_supervisor(MySupervisor, "my_supervisor_test")

wait_for_genserver_sync(server, timeout \\ 1000)

Waits for a GenServer to be alive and responsive.

  • Signature: @spec wait_for_genserver_sync(GenServer.server(), timeout()) :: :ok | {:error, term()}

  • Example:
    wait_for_genserver_sync(server_pid)

wait_for_process_restart(process_name, original_pid, timeout \\ 1000)

Waits for a supervised process to be terminated and restarted.

  • Signature: @spec wait_for_process_restart(atom(), pid(), timeout()) :: {:ok, pid()} | {:error, term()}

  • Example:
    original_pid = GenServer.whereis(MyServer)
    GenServer.stop(MyServer)
    {:ok, new_pid} = wait_for_process_restart(MyServer, original_pid)

Supertester.GenServerHelpers

This module provides helpers specifically for testing GenServers.

get_server_state_safely(server)

Fetches the state of a GenServer without crashing if the process is down.

  • Signature: @spec get_server_state_safely(GenServer.server()) :: {:ok, term()} | {:error, term()}

  • Example:
    {:ok, state} = get_server_state_safely(server_pid)

cast_and_sync(server, cast_message, sync_message \\ :__supertester_sync__, opts \\ [])

Sends a cast message and then waits for a follow-up call to confirm the cast was processed. This is the recommended way to test async operations.

  • Signature: @spec cast_and_sync(GenServer.server(), term(), term(), keyword()) :: :ok | {:ok, term()} | {:error, term()}

  • Options: :strict? (default false) raises when the target server doesn't implement the sync handler; :timeout to customize the sync call timeout.
  • Example:
    :ok = cast_and_sync(counter_pid, :increment)
    assert_genserver_state(counter_pid, fn state -> state.count == 1 end)
    
    # Enforce the presence of the sync handler
    assert_raise ArgumentError do
      cast_and_sync(counter_pid, :increment, :__supertester_sync__, strict?: true)
    end

test_server_crash_recovery(server, crash_reason)

Simulates a process crash and verifies its recovery by its supervisor.

  • Signature: @spec test_server_crash_recovery(GenServer.server(), term()) :: {:ok, map()} | {:error, term()}

  • Example:
    {:ok, info} = test_server_crash_recovery(server_pid, :test_crash)
    assert info.recovered == true

concurrent_calls(server, calls, count \\ 10, opts \\ [])

Stress-tests a GenServer with many concurrent requests.

  • Signature: @spec concurrent_calls(GenServer.server(), [term()], pos_integer(), keyword()) :: {:ok, [map()]} (opts[:timeout] controls the per-call timeout)
  • Example:
    calls = [:get_counter, {:increment, 1}]
    {:ok, results} = concurrent_calls(server_pid, calls, 20, timeout: 50)
    
    Enum.each(results, fn %{call: call, successes: successes, errors: errors} ->
      IO.inspect({call, length(successes), length(errors)})
    end)

Supertester.SupervisorHelpers

This module provides helpers for testing supervision trees and restart strategies.

test_restart_strategy(supervisor, strategy, scenario)

Tests a supervisor's restart strategy (:one_for_one, :one_for_all, etc.) with a given failure scenario.

  • Signature: @spec test_restart_strategy(Supervisor.supervisor(), atom(), restart_scenario()) :: test_result()
  • Example:
    result = test_restart_strategy(supervisor, :one_for_one, {:kill_child, :worker_1})
    assert result.restarted == [:worker_1]
    assert :worker_2 in result.not_restarted

assert_supervision_tree_structure(supervisor, expected)

Validates the structure of a supervision tree.

  • Signature: @spec assert_supervision_tree_structure(Supervisor.supervisor(), tree_structure()) :: :ok
  • Example:
    assert_supervision_tree_structure(root_supervisor, %{
      supervisor: RootSupervisor,
      strategy: :one_for_one,
      children: [
        {:cache, CacheServer},
        {:worker_pool, WorkerPoolSupervisor}
      ]
    })

trace_supervision_events(supervisor, opts \\ [])

Monitors and returns all supervision events (e.g., child started, terminated, restarted) that occur during a test.

  • Signature: @spec trace_supervision_events(Supervisor.supervisor(), keyword()) :: {:ok, (-> [supervision_event()])}
  • Example:
    {:ok, stop_trace} = trace_supervision_events(supervisor)
    # ... cause a failure ...
    events = stop_trace.()
    assert Enum.any?(events, &match?({:child_restarted, _, _, _}, &1))

wait_for_supervisor_stabilization(supervisor, timeout \\ 5000)

Waits for a supervisor to have all its children running and stable, which is useful after inducing failures.

  • Signature: @spec wait_for_supervisor_stabilization(Supervisor.supervisor(), timeout()) :: :ok | {:error, :timeout}

  • Example:
    # ... cause chaos ...
    :ok = wait_for_supervisor_stabilization(supervisor)
    assert_all_children_alive(supervisor)

Chaos Engineering

Supertester.ChaosHelpers

This module provides a toolkit for chaos engineering to test the resilience of your system.

inject_crash(target, crash_spec, opts \\ [])

Injects a controlled crash into a process.

  • Signature: @spec inject_crash(pid(), crash_spec(), keyword()) :: :ok
  • Crash Specifications:
    • :immediate: Crashes the process immediately.
    • {:after_ms, duration}: Crashes after a delay.
    • {:random, probability}: Crashes with a given probability (0.0 to 1.0).
  • Example:
    inject_crash(worker_pid, :immediate)
    inject_crash(worker_pid, {:random, 0.5}) # 50% chance of crash

chaos_kill_children(supervisor, opts \\ [])

Randomly kills children in a supervision tree to test restart strategies and system resilience.

  • Signature: @spec chaos_kill_children(Supervisor.supervisor(), keyword()) :: chaos_report()
  • Options: :kill_rate, :duration_ms, :kill_interval_ms
  • Example:
    report = chaos_kill_children(supervisor, kill_rate: 0.5, duration_ms: 3000)
    assert report.supervisor_crashed == false

simulate_resource_exhaustion(resource, opts \\ [])

Simulates resource exhaustion scenarios, such as process or ETS table limits.

  • Signature: @spec simulate_resource_exhaustion(atom(), keyword()) :: {:ok, cleanup_fn :: (-> :ok)} | {:error, term()}

  • Resources: :process_limit, :ets_tables, :memory
  • Example:
    {:ok, cleanup} = simulate_resource_exhaustion(:process_limit, spawn_count: 1000)
    # ... perform tests under pressure ...
    cleanup.()

assert_chaos_resilient(system, chaos_fn, recovery_fn, opts \\ [])

Asserts that a system recovers from a chaos scenario within a given timeout.

  • Signature: @spec assert_chaos_resilient(pid(), (-> any()), (-> boolean()), keyword()) :: :ok
  • Example:
    assert_chaos_resilient(supervisor,
      fn -> chaos_kill_children(supervisor, kill_rate: 0.5) end,
      fn -> all_workers_are_healthy?(supervisor) end,
      timeout: 10_000
    )

run_chaos_suite(target, scenarios, opts \\ [])

Executes a list of chaos scenarios—now including full Supertester.ConcurrentHarness scenarios— against the same target process or supervisor so you can orchestrate concurrent workloads while injecting faults.

  • Signature: @spec run_chaos_suite(pid(), [map()], keyword()) :: chaos_suite_report()
  • Concurrent Scenarios: Provide %{type: :concurrent, build: fn target -> scenario end} or %{type: :concurrent, scenario: <ConcurrentHarness scenario>} entries to reuse the harness with shared telemetry/reporting.
  • Example:
    scenarios = [
      %{type: :kill_children, kill_rate: 0.4, duration_ms: 500},
      %{
        type: :concurrent,
        build: fn supervisor ->
          Supertester.ConcurrentHarness.simple_genserver_scenario(
            MyWorker,
            [{:cast, :do_work}, {:call, :get_state}],
            3,
            setup: fn -> {:ok, supervisor, %{}} end,
            cleanup: fn _, _ -> :ok end
          )
        end
      }
    ]
    
    report = run_chaos_suite(supervisor, scenarios, timeout: 10_000)
    assert report.failed == 0

Performance Testing

Supertester.PerformanceHelpers

This module provides tools for performance testing and regression detection.

assert_performance(operation, expectations)

Asserts that an operation meets specific performance bounds.

  • Signature: @spec assert_performance((-> any()), keyword()) :: :ok
  • Expectations: :max_time_ms, :max_memory_bytes, :max_reductions
  • Example:
    assert_performance(
      fn -> APIServer.get_user(1) end,
      max_time_ms: 50,
      max_memory_bytes: 500_000
    )

assert_no_memory_leak(iterations, operation, opts \\ [])

Detects memory leaks by running an operation many times and checking for memory growth.

  • Signature: @spec assert_no_memory_leak(pos_integer(), (-> any()), keyword()) :: :ok
  • Example:
    assert_no_memory_leak(10_000, fn ->
      MessageWorker.process(worker, generate_message())
    end)

measure_operation(operation)

Measures the performance metrics of an operation.

  • Signature: @spec measure_operation((-> any())) :: map()
  • Returns: A map with :time_us, :memory_bytes, :reductions, and :result.
  • Example:
    metrics = measure_operation(fn -> complex_calculation() end)
    IO.inspect(metrics)

assert_mailbox_stable(server, opts)

Asserts that a GenServer's mailbox does not grow uncontrollably during an operation.

  • Signature: @spec assert_mailbox_stable(pid(), keyword()) :: :ok
  • Options: :during (the function to execute), :max_size (max allowed mailbox size).
  • Example:
    assert_mailbox_stable(server,
      during: fn -> send_many_messages(server, 1000) end,
      max_size: 50
    )

Concurrency Harness

Supertester.ConcurrentHarness

The concurrency harness lets you describe complex multi-threaded scenarios declaratively. Provide a setup function, thread scripts (lists of operations), optional mailbox monitoring, chaos hooks, performance expectations, and an invariant function. run/1 coordinates each thread, synchronizes casts with Supertester.TestableGenServer, records every event, emits telemetry, and returns a diagnostic report:

scenario =
  Supertester.ConcurrentHarness.simple_genserver_scenario(
    CounterServer,
    [{:cast, :increment}, {:call, :value}],
    4,
    mailbox: [sampling_interval: 1],
    chaos: Supertester.ConcurrentHarness.chaos_inject_crash(),
    performance_expectations: [max_time_ms: 100],
    invariant: fn server, ctx ->
      {:ok, state} = Supertester.GenServerHelpers.get_server_state_safely(server)
      assert state.count >= 0
      assert ctx.metrics.total_operations > 0
    end
  )

assert {:ok, report} = Supertester.ConcurrentHarness.run(scenario)

Use from_property_config/3 to convert property-test generators straight into runnable scenarios. Scenario metadata automatically includes a :scenario_id, and every run emits [:supertester, :concurrent, :scenario, :start|:stop] telemetry events for observability.

Additional helpers:

  • chaos_kill_children/1 / chaos_inject_crash/2 – build ready-made chaos hooks.
  • run_with_performance/2 – wrap an ad-hoc scenario with performance bounds outside of the struct.

Supertester.PropertyHelpers

PropertyHelpers builds on StreamData to emit normalized operations and scenario configs that the concurrent harness understands. genserver_operation_sequence/2 normalizes operations into tagged {:call, term} / {:cast, term} tuples, while concurrent_scenario/1 generates complete configs:

use ExUnitProperties

property "counter invariants hold" do
  generator =
    Supertester.PropertyHelpers.concurrent_scenario(
      operations: [{:cast, :increment}, {:cast, :decrement}, {:call, :value}],
      min_threads: 1,
      max_threads: 3
    )

  check all cfg <- generator do
    scenario = Supertester.ConcurrentHarness.from_property_config(CounterServer, cfg)
    assert {:ok, _report} = Supertester.ConcurrentHarness.run(scenario)
  end
end

If :stream_data is not present in your project, these helpers raise a clear error suggesting the dependency addition.

Supertester.MessageHarness

MessageHarness.trace_messages/3 snapshots a process mailbox, enables :erlang.trace for :receive events, runs your function, then returns the captured messages and result. It is ideal for debugging concurrency issues without changing application code.

report =
  Supertester.MessageHarness.trace_messages(server, fn ->
    send(server, {:direct, :hello})
  end)

assert {:direct, :hello} in report.messages

Telemetry & Diagnostics

Supertester.Telemetry

Supertester emits :telemetry events under the [:supertester | ...] namespace. The helper module centralizes emission so you can subscribe once and observe:

  • [:supertester, :concurrent, :scenario, :start|:stop] – scenario lifecycle with duration/status.
  • [:supertester, :concurrent, :mailbox, :sample] – mailbox measurements collected during runs.
  • [:supertester, :chaos, :start|:stop] – chaos hooks firing, including duration and failures.
  • [:supertester, :performance, :scenario, :measured] – performance metrics captured for scenarios.

Attach handlers with :telemetry.attach/4 or attach_many/4:

:telemetry.attach(
  "supertester-console",
  [:supertester, :concurrent, :scenario, :stop],
  fn _event, %{duration_ms: duration}, metadata, _ ->
    Logger.info("Scenario #{metadata.scenario_id} took #{duration}ms (#{metadata[:status] || :ok})")
  end,
  nil
)

Supertester.TelemetryHelpers

TelemetryHelpers provides per-test telemetry isolation, so your tests only receive events tagged with the current test id. This prevents cross-test noise when running async: true.

{:ok, _test_id} = Supertester.TelemetryHelpers.setup_telemetry_isolation()
{:ok, _handler} = Supertester.TelemetryHelpers.attach_isolated([:my, :event])

Supertester.TelemetryHelpers.emit_with_context([:my, :event], %{value: 1}, %{})
assert Supertester.TelemetryHelpers.assert_telemetry([:my, :event])

Key helpers:

  • setup_telemetry_isolation/0 and setup_telemetry_isolation/1
  • attach_isolated/2 with passthrough, buffer, filter_key, and transform
  • assert_telemetry/1-3, refute_telemetry/2, assert_telemetry_count/2, flush_telemetry/1
  • with_telemetry/2 and emit_with_context/3

Supertester.LoggerIsolation

LoggerIsolation provides process-scoped logger levels and log capture helpers.

:ok = Supertester.LoggerIsolation.setup_logger_isolation()
Supertester.LoggerIsolation.isolate_level(:debug)

{log, _result} =
  Supertester.LoggerIsolation.capture_isolated(:debug, fn ->
    Logger.debug("hello")
    :ok
  end)

Key helpers:

  • setup_logger_isolation/0 and setup_logger_isolation/1
  • isolate_level/1, restore_level/0, get_isolated_level/0
  • capture_isolated/2, capture_isolated!/2, with_level/2, with_level_and_capture/2

Supertester.ETSIsolation

ETSIsolation enables per-test ETS tables, mirroring, and safe injections.

:ok = Supertester.ETSIsolation.setup_ets_isolation()
{:ok, table} = Supertester.ETSIsolation.create_isolated(:set, name: :temp_table)

{:ok, restore} =
  Supertester.ETSIsolation.inject_table(MyModule, :table_name, :temp_table)

restore.()

Key helpers:

  • setup_ets_isolation/0-2
  • create_isolated/1-2, mirror_table/1-2
  • inject_table/3-4, with_table/2-3

Custom Assertions

Supertester.Assertions

This module provides a set of custom, OTP-aware assertions.

  • assert_process_alive(pid) / assert_process_dead(pid)
  • assert_process_restarted(process_name, original_pid)
  • assert_genserver_state(server, expected_state_or_fun)
  • assert_genserver_responsive(server)
  • assert_child_count(supervisor, expected_count)
  • assert_all_children_alive(supervisor)
  • assert_no_process_leaks(operation_fun)
  • assert_memory_usage_stable(operation_fun, tolerance)

Example: assert_genserver_state/2

This assertion can take an exact state or a function to validate the state.

# Exact match
assert_genserver_state(server, %{count: 5})

# Function validation
assert_genserver_state(server, fn state ->
  state.count > 0 and state.status == :active
end)

Practical Examples & Recipes

Basic GenServer Test

defmodule MyApp.CounterTest do
  use ExUnit.Case, async: true
  import Supertester.{OTPHelpers, GenServerHelpers, Assertions}

  test "counter increments correctly" do
    {:ok, counter} = setup_isolated_genserver(Counter)
    :ok = cast_and_sync(counter, :increment)
    assert_genserver_state(counter, fn s -> s.count == 1 end)
  end
end

Supervisor Restart Strategy Test

defmodule MyApp.MySupervisorTest do
  use ExUnit.Case, async: true
  import Supertester.{OTPHelpers, SupervisorHelpers, Assertions}

  test "one_for_one strategy restarts only the failed child" do
    {:ok, supervisor} = setup_isolated_supervisor(MySupervisor)
    result = test_restart_strategy(supervisor, :one_for_one, {:kill_child, :worker_1})
    assert result.restarted == [:worker_1]
    wait_for_supervisor_stabilization(supervisor)
    assert_all_children_alive(supervisor)
  end
end

Chaos Test for System Resilience

defmodule MyApp.ResilienceTest do
  use ExUnit.Case, async: true
  import Supertester.{OTPHelpers, ChaosHelpers, Assertions}

  test "system survives random worker crashes" do
    {:ok, supervisor} = setup_isolated_supervisor(WorkerSupervisor)
    report = chaos_kill_children(supervisor, kill_rate: 0.5, duration_ms: 2000)
    assert Process.alive?(supervisor)
    assert_all_children_alive(supervisor)
  end
end

Performance SLA Test

defmodule MyApp.PerformanceTest do
  use ExUnit.Case, async: true
  import Supertester.{OTPHelpers, PerformanceHelpers}

  test "API endpoint meets performance SLA" do
    {:ok, api_server} = setup_isolated_genserver(APIServer)
    assert_performance(
      fn -> APIServer.get_user(api_server, 123) end,
      max_time_ms: 100,
      max_memory_bytes: 1_000_000
    )
  end
end

Memory Leak Detection

defmodule MyApp.MemoryTest do
  use ExUnit.Case, async: true
  import Supertester.{OTPHelpers, PerformanceHelpers}

  test "worker does not leak memory" do
    {:ok, worker} = setup_isolated_genserver(Worker)
    assert_no_memory_leak(10_000, fn ->
      Worker.process(worker, generate_message())
    end)
  end
end

Best Practices

  1. Always Use Isolation: Start your test modules with use Supertester.ExUnitFoundation, isolation: :full_isolation.
  2. Prefer setup_isolated_*: Use setup_isolated_genserver and setup_isolated_supervisor to ensure automatic cleanup and unique naming.
  3. Avoid Process.sleep: Use cast_and_sync for asynchronous operations and wait_for_* helpers for other synchronization needs.
  4. Use Expressive Assertions: Leverage the custom assertions in Supertester.Assertions to make your tests clearer and more concise.
  5. Test for Resilience: Use the ChaosHelpers to inject faults and ensure your system can handle them gracefully.
  6. Assert Performance: Use PerformanceHelpers to set performance SLAs and prevent regressions.

Troubleshooting

  • Flaky Tests: If your tests are still flaky, ensure every asynchronous operation is followed by a synchronization helper like cast_and_sync.
  • Name Conflicts: If you encounter name clashes, make sure you are using setup_isolated_genserver for all your processes.
  • Supervisor Tests Failing: After inducing failures in a supervisor test, always use wait_for_supervisor_stabilization before making assertions about its children.
  • Inconsistent Performance Tests: Run :erlang.garbage_collect() before measuring performance and use a sufficient number of iterations to get stable results.