elixir-tdd
Test-driven development enforcement for Elixir and Phoenix. Requires failing tests before implementation. Use when implementing features, fixing bugs, or when code quality discipline is needed.
Works with
Agent Skills format with YAML frontmatter. Claude Code reads it as-is.
---
name: "elixir-tdd"
description: "Test-driven development enforcement for Elixir and Phoenix. Requires failing tests before implementation. Use when implementing features, fixing bugs, or when code quality discipline is needed."
license: "MIT"
---
# Elixir TDD Enforcement
Strict test-driven development practices for Elixir and Phoenix projects.
## The Golden Rule
**No Code Without a Failing Test First**
This is not optional. This is not negotiable. Every feature, every bug fix, every change starts with a test.
## The TDD Cycle
1. **Red**: Write a test that describes the behavior you want. Run it. It must fail.
2. **Green**: Write the minimum code to make the test pass. Nothing more.
3. **Refactor**: Clean up while keeping tests green.
4. **Repeat**
```elixir
# Step 1: Write the failing test
test "create_user/1 with valid attrs creates a user" do
attrs = %{email: "test@example.com", name: "Test User"}
assert {:ok, %User{} = user} = Accounts.create_user(attrs)
assert user.email == "test@example.com"
end
# Step 2: Run it - it MUST fail
# $ mix test test/my_app/accounts_test.exs:10
# ** (UndefinedFunctionError) function Accounts.create_user/1 is undefined
# Step 3: Write minimum code to pass
def create_user(attrs) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
# Step 4: Run test again - it passes
# Step 5: Refactor if needed, keeping tests green
```
## Test File Structure
Match your source structure:
```
lib/my_app/accounts/user.ex → test/my_app/accounts/user_test.exs
lib/my_app/accounts.ex → test/my_app/accounts_test.exs
lib/my_app_web/live/task_live.ex → test/my_app_web/live/task_live_test.exs
lib/my_app_web/controllers/ → test/my_app_web/controllers/
```
## Mandatory Test Cases
For every function, test:
### 1. Happy Path
Valid input produces expected output.
```elixir
test "create_task/1 with valid attrs creates task" do
attrs = %{title: "Test Task", status: :todo}
assert {:ok, %Task{} = task} = Tasks.create_task(attrs)
assert task.title == "Test Task"
assert task.status == :todo
end
```
### 2. Validation Failures
Invalid input returns error changeset.
```elixir
test "create_task/1 with missing title returns error" do
assert {:error, %Ecto.Changeset{} = changeset} = Tasks.create_task(%{})
assert %{title: ["can't be blank"]} = errors_on(changeset)
end
test "create_task/1 with invalid status returns error" do
attrs = %{title: "Test", status: :invalid_status}
assert {:error, changeset} = Tasks.create_task(attrs)
assert %{status: ["is invalid"]} = errors_on(changeset)
end
```
### 3. Edge Cases
Boundary conditions and unusual inputs.
```elixir
test "create_task/1 with empty string title returns error" do
assert {:error, changeset} = Tasks.create_task(%{title: ""})
assert %{title: ["can't be blank"]} = errors_on(changeset)
end
test "create_task/1 with whitespace-only title returns error" do
assert {:error, changeset} = Tasks.create_task(%{title: " "})
assert %{title: ["can't be blank"]} = errors_on(changeset)
end
test "list_tasks/0 returns empty list when no tasks exist" do
assert [] = Tasks.list_tasks()
end
```
### 4. Authorization
Users can only access their own resources.
```elixir
test "get_task/2 returns error when task belongs to another user" do
other_user = user_fixture()
task = task_fixture(user_id: other_user.id)
user = user_fixture()
assert {:error, :not_found} = Tasks.get_task(user, task.id)
end
test "update_task/3 returns error when user doesn't own task" do
owner = user_fixture()
other_user = user_fixture()
task = task_fixture(user_id: owner.id)
assert {:error, :unauthorized} = Tasks.update_task(other_user, task, %{title: "Hacked"})
end
```
### 5. State Transitions (if applicable)
Valid and invalid state changes.
```elixir
describe "transition_task/2" do
test "allows todo -> in_progress" do
task = task_fixture(status: :todo)
assert {:ok, task} = Tasks.transition_task(task, :in_progress)
assert task.status == :in_progress
end
test "allows in_progress -> done" do
task = task_fixture(status: :in_progress)
assert {:ok, task} = Tasks.transition_task(task, :done)
assert task.status == :done
end
test "rejects todo -> done (must go through in_progress)" do
task = task_fixture(status: :todo)
assert {:error, :invalid_transition} = Tasks.transition_task(task, :done)
end
test "rejects done -> todo" do
task = task_fixture(status: :done)
assert {:error, :invalid_transition} = Tasks.transition_task(task, :todo)
end
end
```
## Testing LiveView
Use `Phoenix.LiveViewTest` for integration tests.
```elixir
import Phoenix.LiveViewTest
describe "TaskLive.Index" do
test "renders task list", %{conn: conn} do
task = task_fixture(title: "My Task")
{:ok, view, html} = live(conn, ~p"/tasks")
assert html =~ "My Task"
assert has_element?(view, "#task-#{task.id}")
end
test "creates new task", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/tasks")
view
|> form("#task-form", task: %{title: "New Task"})
|> render_submit()
assert has_element?(view, "#tasks", "New Task")
end
test "validates task on change", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/tasks/new")
html =
view
|> form("#task-form", task: %{title: ""})
|> render_change()
assert html =~ "can't be blank"
end
test "deletes task", %{conn: conn} do
task = task_fixture(title: "Delete Me")
{:ok, view, _html} = live(conn, ~p"/tasks")
view
|> element("#task-#{task.id} button", "Delete")
|> render_click()
refute has_element?(view, "#task-#{task.id}")
end
end
```
## Test Data
Use factories or fixture functions. Keep them minimal.
```elixir
# test/support/fixtures/accounts_fixtures.ex
defmodule MyApp.AccountsFixtures do
def user_fixture(attrs \\ %{}) do
{:ok, user} =
attrs
|> Enum.into(%{
email: "user#{System.unique_integer()}@example.com",
name: "Test User"
})
|> MyApp.Accounts.create_user()
user
end
end
# Or with ExMachina
defmodule MyApp.Factory do
use ExMachina.Ecto, repo: MyApp.Repo
def user_factory do
%MyApp.Accounts.User{
email: sequence(:email, &"user#{&1}@example.com"),
name: "Test User"
}
end
end
```
## What NOT to Do
### ❌ Don't write tests after the code
```elixir
# WRONG: Code first, then tests
def create_task(attrs), do: ... # Written first
test "create_task works" do ... # Added later to "cover" it
```
### ❌ Don't skip tests for "simple" functions
```elixir
# WRONG: "It's too simple to test"
def full_name(user), do: "#{user.first_name} #{user.last_name}"
# Still needs tests! What if first_name is nil?
```
### ❌ Don't test private functions directly
```elixir
# WRONG: Testing private implementation
test "parse_date/1 parses ISO format" do
assert MyModule.parse_date("2024-01-01") == ~D[2024-01-01]
end
# RIGHT: Test through the public API
test "create_event/1 accepts ISO date strings" do
assert {:ok, event} = Events.create_event(%{date: "2024-01-01"})
assert event.date == ~D[2024-01-01]
end
```
### ❌ Don't mock Ecto or database in context tests
```elixir
# WRONG: Mocking the repo
expect(Repo, :insert, fn _ -> {:ok, %User{}} end)
# RIGHT: Use the sandbox, test real behavior
assert {:ok, %User{}} = Accounts.create_user(valid_attrs)
assert Repo.get(User, user.id) # Actually in database
```
### ❌ Don't write tests that pass regardless of implementation
```elixir
# WRONG: Test always passes
test "does something" do
result = MyModule.do_thing()
assert result # What if result is {:error, ...}? Still truthy!
end
# RIGHT: Assert specific expectations
test "returns ok tuple with user" do
assert {:ok, %User{email: "test@example.com"}} = MyModule.do_thing()
end
```
## Pre-Implementation Checklist
Before writing ANY code, ask yourself:
1. ☐ Have I written a failing test?
2. ☐ Does the test describe the behavior I want?
3. ☐ Have I run the test and confirmed it fails?
4. ☐ Does it fail for the RIGHT reason?
Only after checking all boxes: write the implementation.
## Test Organization
```elixir
defmodule MyApp.TasksTest do
use MyApp.DataCase
alias MyApp.Tasks
alias MyApp.Tasks.Task
import MyApp.AccountsFixtures
import MyApp.TasksFixtures
describe "create_task/1" do
test "with valid attrs creates task" do
# ...
end
test "with invalid attrs returns error changeset" do
# ...
end
test "with empty title returns error" do
# ...
end
end
describe "update_task/2" do
setup do
task = task_fixture()
%{task: task}
end
test "with valid attrs updates the task", %{task: task} do
# ...
end
end
describe "delete_task/1" do
# ...
end
end
```
## Mocking External Dependencies with Mox
Use behaviours + Mox for external services. Never mock Ecto or internal modules.
```elixir
# 1. Define a behaviour
defmodule MyApp.WeatherAPI do
@callback get_forecast(String.t()) :: {:ok, map()} | {:error, term()}
end
# 2. Create the real implementation
defmodule MyApp.WeatherAPI.Client do
@behaviour MyApp.WeatherAPI
@impl true
def get_forecast(city) do
Req.get("https://api.weather.com/forecast", params: [city: city])
end
end
# 3. Configure the mock in test_helper.exs
Mox.defmock(MyApp.MockWeatherAPI, for: MyApp.WeatherAPI)
# 4. Configure in config/test.exs
# config :my_app, weather_api: MyApp.MockWeatherAPI
# 5. Use in your context (reads from config)
defmodule MyApp.Weather do
def weather_api, do: Application.get_env(:my_app, :weather_api, MyApp.WeatherAPI.Client)
def get_forecast(city) do
weather_api().get_forecast(city)
end
end
# 6. Test with mock
import Mox
test "get_forecast returns weather data" do
expect(MyApp.MockWeatherAPI, :get_forecast, fn "London" ->
{:ok, %{temp: 15, condition: "cloudy"}}
end)
assert {:ok, %{temp: 15}} = Weather.get_forecast("London")
end
```
## Property-Based Testing with StreamData
Test properties that hold for all inputs, not just specific examples:
```elixir
use ExUnitProperties
# ✅ Good: Test a property that always holds
property "User.full_name/1 always returns a string" do
check all first <- string(:alphanumeric, min_length: 1),
last <- string(:alphanumeric, min_length: 1) do
user = %User{first_name: first, last_name: last}
result = User.full_name(user)
assert is_binary(result)
assert String.contains?(result, first)
assert String.contains?(result, last)
end
end
# ✅ Good: Roundtrip property
property "encoding then decoding returns the original" do
check all data <- map_of(string(:alphanumeric), integer()) do
assert data == data |> MyApp.Encoder.encode() |> MyApp.Encoder.decode()
end
end
# ✅ Good: Invariant property
property "sorting is idempotent" do
check all list <- list_of(integer()) do
sorted = Enum.sort(list)
assert sorted == Enum.sort(sorted)
end
end
# ✅ Good: Custom generators for domain types
property "valid emails are accepted, invalid rejected" do
valid_email = gen all name <- string(:alphanumeric, min_length: 1),
domain <- string(:alphanumeric, min_length: 1) do
"#{name}@#{domain}.com"
end
check all email <- valid_email do
assert {:ok, _} = Accounts.validate_email(email)
end
end
```
## Running Tests
```bash
# Run all tests
mix test
# Run specific file
mix test test/my_app/tasks_test.exs
# Run specific test by line number
mix test test/my_app/tasks_test.exs:42
# Run with coverage
mix test --cover
# Run failed tests only
mix test --failed
# Run tests matching a pattern
mix test --only integration
```More General & Other skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
grill-me
mattpocock/skills
A relentless interview to sharpen a plan or design.
grill-with-docs
mattpocock/skills
A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.

