From elixir-phoenix
Best practices for testing Elixir applications with Ecto.SQL.Sandbox, including background process handling, Oban testing, and test output quality. Use when writing or reviewing Elixir tests.
How this skill is triggered — by the user, by Claude, or both
Slash command
/elixir-phoenix:elixir-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This guide covers production-ready patterns for testing Elixir applications with Ecto.SQL.Sandbox, particularly when dealing with background processes (GenServers, Oban workers, etc.) that need database access.
This guide covers production-ready patterns for testing Elixir applications with Ecto.SQL.Sandbox, particularly when dealing with background processes (GenServers, Oban workers, etc.) that need database access.
The modern best practice is to use :manual mode globally with selective shared mode via test tags:
# test/support/data_case.ex
setup tags do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(YourApp.Repo,
shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
:ok
end
This provides:
async: falsestart_owner!/2| Feature | Manual Mode | Shared Mode | Recommendation |
|---|---|---|---|
| Async tests | Yes | No | Manual is better |
| Test performance | Fast | Slow | Manual is better |
| Background processes | Requires allow/3 | Auto-access | Depends on use case |
| Explicitness | Clear | Implicit | Manual is better |
| Setup complexity | Medium | Low | Shared is simpler |
| Production use | Recommended | Selective use | Use manual |
Decision Matrix:
async: true (manual mode, no allowances needed)async: true with explicit allow/3async: false (auto-shared mode)Problem:
# test/test_helper.exs
Ecto.Adapters.SQL.Sandbox.mode(YourApp.Repo, {:shared, self()})
Why it's bad:
async: falseSolution:
Use manual mode with selective shared mode via start_owner!/2 pattern.
Problem:
# lib/your_app/application.ex (WRONG!)
defp children do
if Mix.env() == :test do
[]
else
[YourApp.IPAM]
end
end
Why it's bad:
Mix is not available in releasesSolution: Use application configuration or configure via test setup:
# Use start_supervised! in tests that need the process
test "test that needs IPAM" do
start_supervised!(YourApp.IPAM)
# Test code
end
Recommended Configuration:
# config/test.exs
config :your_app, Oban,
testing: :manual, # Jobs persist, use perform_job/3 to execute
plugins: false # Prevent background plugins from starting
Why this configuration:
Ecto.Adapters.SQL.Sandbox.mode(Repo, :manual))start_owner!/2 instead of checkout/2 (modern pattern)shared: not tags[:async]start_supervised!/1 for automatic process cleanuptesting: :manual and plugins: falseMix.env() in production code (lib/ directory)start_supervised! or on_exitcapture_log/1 or capture_io/1Test runs MUST produce zero warnings and zero error log output. This is not optional - noisy test output masks real problems and makes debugging significantly harder.
When your code intentionally produces log output (errors, warnings, info), you MUST silence it in tests using ExUnit.CaptureLog or ExUnit.CaptureIO.
Correct pattern - capture and assert:
import ExUnit.CaptureLog
test "handles failure gracefully" do
log = capture_log(fn ->
result = SomeWorker.perform(%{will: "fail"})
assert {:error, _} = result
end)
assert log =~ "Operation failed"
assert log =~ "Expected error message"
end
Correct pattern - silence expected info logs:
test "successful operation" do
capture_log(fn ->
result = SomeWorker.perform(%{will: "succeed"})
assert {:ok, _} = result
end)
end
WRONG - letting logs pollute test output:
test "handles failure" do
# This will spam error logs to console!
result = SomeWorker.perform(%{will: "fail"})
assert {:error, _} = result
end
capture_log/1 - For Logger calls (Logger.error, Logger.info, etc.)capture_io/1 - For IO calls (IO.puts, IO.warn, etc.)capture_io/2 - For capturing specific devices (:stderr, :stdio)capture_log/1 when testing code that intentionally logsExUnit.CaptureLog at the top of test modules that need itdefmodule YourApp.WorkerTest do
use YourApp.DataCase, async: false
import ExUnit.CaptureLog
test "reserves resource before operation to prevent races" do
{:ok, resource} = create_resource()
expect(MockService, :do_operation, fn ->
assert get_resource!(resource.id).status == "reserved"
{:error, "Operation failed"}
end)
log = capture_log(fn ->
result = Worker.perform(%{resource_id: resource.id})
assert {:error, _} = result
end)
assert log =~ "Worker failed"
assert log =~ "Operation failed"
assert get_resource!(resource.id).status == "failed"
assert get_resource!(resource.id).reserved_id != nil
end
end
npx claudepluginhub code0100fun/botfiles --plugin elixir-phoenixGuides Elixir testing with ExUnit: unit/integration/property-based tests, assertions, mocks, fixtures, tags, describe blocks, setup, and concurrent code testing.
Provides Elixir testing patterns for ExUnit, Mox mocks, factories, and LiveView helpers. Use on *_test.exs files, test/support/, factories, or fixing test failures.
Guides Elixir testing with ExUnit: writing unit tests, organizing test suites, using assertions, setup/teardown, and test tags.