Agent skill

test-generation

Generates comprehensive Elixir tests using ExUnit following TDD principles.

Stars 0
Forks 1

Install this agent skill to your Project

npx add-skill https://github.com/layeddie/ai-rules/tree/main/skills/test-generation

SKILL.md

Test Generation Skill

Use this skill to generate Elixir tests using ExUnit with TDD workflow.

When to Use

  • Writing tests for new features
  • Creating test coverage for existing code
  • Implementing test strategies for complex logic
  • Writing property-based tests for edge cases

TDD Workflow

Red - Write Failing Test

Write a test that describes the desired behavior, then implement the feature to make it pass.

elixir
# 1. Red - Write failing test
defmodule Accounts.User.CreateTest do
  use Accounts.DataCase

  test "creates user with valid attributes" do
    attrs = %{email: "test@example.com", password: "password123"}
    assert {:ok, %User{} = user} = Accounts.User.Create.call(attrs)
    assert user.email == "test@example.com"
  end
end

Green - Make Test Pass

Implement the minimal code needed to make the test pass.

elixir
# 2. Green - Make test pass
defmodule Accounts.User.Create do
  alias Accounts.{User, Repo}

  @spec call(map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
  def call(attrs) do
    %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
  end
end

Refactor - Improve Code

Refactor the code with confidence, knowing tests will catch any regressions.

elixir
# 3. Refactor - Improve with confidence
defmodule Accounts.User.Create do
  alias Accounts.{User, Repo}

  @spec call(map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
  def call(attrs) do
    %User{}
    |> User.changeset(attrs)
    |> put_password_hash()
    |> Repo.insert()
  end

  defp put_password_hash(changeset) do
    case get_change(changeset, :password) do
      {nil, _} -> changeset
      {password, changeset} ->
        hash = Bcrypt.hash_pwd(password)
        put_change(changeset, :password_hash, hash)
    end
  end
end

Test Patterns

Unit Test Pattern

elixir
defmodule Accounts.User.CreateTest do
  use Accounts.DataCase

  describe "call/1" do
    test "creates user with valid attributes" do
      attrs = %{email: "test@example.com", password: "password123"}
      assert {:ok, %User{} = user} = Accounts.User.Create.call(attrs)
      assert user.email == "test@example.com"
    end

    test "returns error with duplicate email" do
      # Setup: Create existing user
      attrs = %{email: "test@example.com", password: "password456"}
      {:ok, _existing} = Accounts.User.Create.call(attrs)

      # Action: Try to create duplicate
      assert {:error, %Ecto.Changeset{} = changeset} =
               Accounts.User.Create.call(attrs)

      # Assertion: Check error message
      assert %{email: ["has already been taken"]} = errors_on(changeset)
    end

    test "returns error with invalid email format" do
      attrs = %{email: "invalid", password: "password123"}
      assert {:error, %Ecto.Changeset{} = changeset} =
               Accounts.User.Create.call(attrs)
      assert %{email: ["has invalid format"]} = errors_on(changeset)
    end
  end
end

GenServer Test Pattern

elixir
defmodule Cache.WorkerTest do
  use ExUnit.Case, async: false
  alias Cache.Worker

  setup do
    {:ok, pid} = start_supervised!(Cache.Worker)
    %{pid: pid}
  end

  describe "get/1" do
    test "returns stored value", %{pid: pid} do
      Cache.Worker.put(:test_key, "test_value")
      assert Cache.Worker.get(:test_key) == "test_value"
    end

    test "returns nil for missing key", %{pid: pid} do
      assert is_nil(Cache.Worker.get(:missing_key))
    end
  end

  describe "put/2" do
    test "stores value successfully", %{pid: pid} do
      assert :ok = Cache.Worker.put(:new_key, "new_value")
      assert Cache.Worker.get(:new_key) == "new_value"
    end
  end
end

Property-Based Testing Pattern

elixir
defmodule StringProcessorTest do
  use ExUnit.Case
  use PropCheck

  describe "reverse/1" do
    property "reversing twice returns original" do
      forall {str} <- term() do
        str
        |> StringProcessor.reverse()
        |> StringProcessor.reverse()
        == str
      end
    end
  end
end

Test Organization

Directory Structure

test/
└── my_app/
    └── accounts/
        └── user/
            ├── create_test.exs
            ├── update_test.exs
            └── delete_test.exs

Test Naming Convention

<feature>_<resource>_<action>_test.exs

Example: user_registration_test.exs, post_publish_test.exs

Test Coverage Goals

Aim for:

  • Business Logic: 80%+ coverage
  • Domain Modules: 85%+ coverage
  • Integration Points: 90%+ coverage
  • Critical Paths: 100% coverage

Commands to Run

bash
# Run all tests
mix test

# Run tests with coverage
mix test --cover

# View coverage report
open cover/excoveralls.html

# Run specific test file
mix test test/accounts/user/create_test.exs

# Run tests with trace output
mix test --trace

Best Practices

Do

  • Write failing test first (TDD Red)
  • Implement minimal code to make test pass (TDD Green)
  • Refactor with confidence (TDD Refactor)
  • Test happy paths and error cases
  • Test edge cases and boundaries
  • Use descriptive test names
  • Organize tests by feature/resource

Don't

  • Skip TDD (write tests after implementation)
  • Implement before writing tests
  • Write tests that only test implementation details
  • Use vague or non-descriptive test names
  • Skip testing error cases
  • Ignore test failures
  • Write tests that are fragile or hard to maintain

Use this skill to follow TDD and write comprehensive tests.

Didn't find tool you were looking for?

Be as detailed as possible for better results