Claude-skill-registry elixir-tdd-enforcement
MANDATORY for ANY feature or bugfix - write ExUnit test FIRST, watch it FAIL, then implement. NO exceptions. Use before writing any Elixir production code.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/elixir-tdd-enforcement" ~/.claude/skills/majiayu000-claude-skill-registry-elixir-tdd-enforcement && rm -rf "$T"
skills/data/elixir-tdd-enforcement/SKILL.mdElixir TDD Enforcement: The Iron Law
THE IRON LAW
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
Not sometimes. Not usually. ALWAYS.
If you write production code before a failing test, DELETE IT and start over.
WHEN THIS SKILL APPLIES
- Implementing ANY new function
- Fixing ANY bug
- Adding ANY feature
- Modifying ANY behavior
- Refactoring ANY code
If you're changing
files in .ex
, this skill is MANDATORY.lib/
THE RED-GREEN-REFACTOR CYCLE
Phase 1: RED (Write Failing Test)
-
Write ONE minimal ExUnit test
test "creates user with valid attrs" do attrs = %{name: "Alice", email: "alice@example.com"} assert {:ok, %User{} = user} = Accounts.create_user(attrs) assert user.name == "Alice" assert user.email == "alice@example.com" end -
Run the test
mix test test/my_app/accounts_test.exs:42 -
VERIFY IT FAILS FOR THE RIGHT REASON
- Read the error message
- Confirm it's failing because functionality doesn't exist
- NOT because of syntax errors or wrong test setup
CHECKPOINT: If test doesn't fail, delete it and write a different test.
Phase 2: GREEN (Minimal Implementation)
-
Write SIMPLEST code to pass the test
def create_user(attrs) do %User{} |> User.changeset(attrs) |> Repo.insert() end -
Run the test again
mix test test/my_app/accounts_test.exs:42 -
VERIFY IT PASSES
- Read the actual output
- See the green dot or "1 test, 0 failures"
- NOT just assume it works
CHECKPOINT: If test doesn't pass, fix implementation (not test).
Phase 3: REFACTOR (Improve While Green)
-
Improve code quality
- Extract functions
- Improve names
- Add pattern matching
-
Run tests after EACH change
mix test -
Stay GREEN
- If tests fail during refactor, undo
- Only refactor when all tests pass
CHECKPOINT: Tests must stay green throughout refactoring.
VERIFICATION CHECKLIST
Before claiming you're done, verify:
- I wrote the test BEFORE any implementation code
- I watched the test FAIL for the right reason
- I read the actual failure message
- I implemented only enough code to pass the test
- I ran the test again and saw it PASS
- I read the actual success message
- All other tests still pass
- I refactored only while tests were green
If you can't check ALL boxes, you didn't follow TDD.
COMMON VIOLATIONS AND RESPONSES
Violation: "I'll just write the code, then write the test"
Response: NO. Delete the code. Write test first.
Violation: "The function is simple, I don't need to see it fail"
Response: WRONG. Even simple code needs failing tests. Write test, watch fail.
Violation: "I already know what the test will look like"
Response: Irrelevant. Write it first anyway.
Violation: "I wrote the test and implementation together"
Response: Delete both. Write test, watch fail, then implement.
Violation: "The test passed on first run"
Response: RED FLAG. Test might not be testing anything. Review test.
Violation: "I'm just refactoring, I don't need new tests"
Response: Correct - but ALL existing tests must stay GREEN.
ELIXIR-SPECIFIC TEST PATTERNS
Testing Context Functions
# RED: Write test first test "list_users/0 returns all users" do user1 = fixture(:user) user2 = fixture(:user) users = Accounts.list_users() assert length(users) == 2 assert user1 in users assert user2 in users end # Run test → watch it fail (function doesn't exist) # GREEN: Implement def list_users do Repo.all(User) end # Run test → watch it pass
Testing Changesets
# RED: Write test for validation test "changeset with invalid email" do changeset = User.changeset(%User{}, %{email: "invalid"}) refute changeset.valid? assert %{email: ["invalid format"]} = errors_on(changeset) end # Run test → watch it fail # GREEN: Add validation def changeset(user, attrs) do user |> cast(attrs, [:email]) |> validate_format(:email, ~r/@/) end
Testing Phoenix Controllers
# RED: Write test test "GET /users returns 200", %{conn: conn} do conn = get(conn, ~p"/users") assert html_response(conn, 200) end # Run test → watch it fail (route doesn't exist) # GREEN: Add route and controller action
DIALYZER ERRORS: SPECIAL CASE
If Dialyzer reports an error:
- Write a test that exercises the problematic code
- Make sure test passes (proving code works)
- Add @spec to guide Dialyzer
- Run
to verifymix dialyzer
NEVER:
- Add to dialyzer.ignore
- Modify dialyzer PLT to suppress
- Comment out the code
The test proves it works. The spec helps Dialyzer understand.
CREDO WARNINGS: SPECIAL CASE
If Credo reports a warning:
- Understand WHY it's warning
- Fix the actual issue (complexity, style, etc.)
- Run
to verifymix credo
NEVER:
- Add to .credo.exs disabled list
- Use inline
# credo:disable-for-this-file - Ignore the warning
Credo is helping you write better code. Listen to it.
THE DISCIPLINE
TDD feels slow at first. That's because you're used to:
- Writing code fast (then debugging for hours)
- Skipping tests (then breaking things in production)
- Guessing if it works (then finding out it doesn't)
TDD is actually faster because:
- Tests catch bugs immediately
- You know exactly what to implement
- Refactoring is safe
- Code works the first time
ENFORCEMENT
Before writing ANY Elixir production code, ask:
- "Have I written a failing test for this?"
- "Have I actually RUN the test and seen it fail?"
- "Do I know WHY it's failing?"
If any answer is NO → write the test first.
REMEMBER
"Tests that pass on the first run might not be testing anything."
"Code without a failing test first is guess-driven development."
"TDD is slow. Debugging untested code is slower."
THE RULE
RED → GREEN → REFACTOR
Not GREEN → RED → "oops"
Not WRITE → PRAY → DEBUG
RED → GREEN → REFACTOR
Every. Single. Time.