---
name: elixir-testing
description: ExUnit testing patterns, Mox for mocking, StreamData for property-based testing, and Phoenix test cases for Elixir applications.
---

# Elixir Testing

## When to Activate

Use this skill when:
- Writing ExUnit tests for Elixir or Phoenix applications
- Setting up Mox for mock-based testing
- Implementing property-based tests with StreamData
- Writing Phoenix controller or LiveView tests
- Setting up test factories with ExMachina
- Debugging flaky concurrent tests in Elixir
- Setting up code coverage with excoveralls
- Testing GenServer behavior and OTP processes

## ExUnit Basics

```elixir
defmodule MyApp.CalculatorTest do
  use ExUnit.Case, async: true  # Always async: true unless DB or shared state

  describe "add/2" do
    test "adds two positive numbers" do
      assert Calculator.add(1, 2) == 3
    end

    test "handles negative numbers" do
      assert Calculator.add(-1, 1) == 0
    end
  end

  describe "divide/2" do
    test "returns {:error, :division_by_zero} when divisor is 0" do
      assert {:error, :division_by_zero} = Calculator.divide(10, 0)
    end
  end
end
```

## Mox for Compile-Safe Mocks

```elixir
# 1. Define behaviour
defmodule MyApp.Payments.Gateway do
  @callback charge(amount :: integer(), token :: String.t()) ::
    {:ok, map()} | {:error, String.t()}
end

# 2. Implementation
defmodule MyApp.Payments.StripeGateway do
  @behaviour MyApp.Payments.Gateway

  def charge(amount, token) do
    Stripe.Charge.create(%{amount: amount, source: token, currency: "usd"})
  end
end

# 3. Register mock in test/test_helper.exs
Mox.defmock(MyApp.MockGateway, for: MyApp.Payments.Gateway)

# 4. Configure application to use mock in test env
# config/test.exs
config :my_app, :payment_gateway, MyApp.MockGateway

# 5. Use in tests
defmodule MyApp.OrderServiceTest do
  use ExUnit.Case, async: true
  import Mox

  setup :verify_on_exit!

  test "processes payment on order creation" do
    expect(MyApp.MockGateway, :charge, fn 1000, "tok_test" -> {:ok, %{id: "ch_123"}} end)

    assert {:ok, order} = OrderService.create(%{amount: 1000, token: "tok_test"})
    assert order.payment_id == "ch_123"
  end
end
```

## DataCase for Database Tests

```elixir
# test/support/data_case.ex (generated by mix phx.new)
defmodule MyApp.DataCase do
  use ExUnit.CaseTemplate

  using do
    quote do
      alias MyApp.Repo
      import Ecto
      import Ecto.Changeset
      import Ecto.Query
      import MyApp.DataCase
    end
  end

  setup tags do
    MyApp.DataCase.setup_sandbox(tags)
    :ok
  end

  def setup_sandbox(tags) do
    pid = Ecto.Adapters.SQL.Sandbox.start_owner!(MyApp.Repo, shared: not tags[:async])
    on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
  end
end

# Usage
defmodule MyApp.AccountsTest do
  use MyApp.DataCase

  test "creates user with valid attrs" do
    assert {:ok, user} = Accounts.create_user(%{email: "user@example.com"})
    assert user.id
  end
end
```

## ExMachina for Test Factories

```elixir
# test/support/factory.ex
defmodule MyApp.Factory do
  use ExMachina.Ecto, repo: MyApp.Repo

  def user_factory do
    %MyApp.Accounts.User{
      email: sequence(:email, &"user#{&1}@example.com"),
      password_hash: Bcrypt.hash_pwd_salt("password123"),
      role: :user
    }
  end

  def admin_factory do
    struct!(user_factory(), role: :admin)
  end

  def post_factory do
    %MyApp.Blog.Post{
      title: "Test Post #{System.unique_integer()}",
      body: "Body content",
      author: build(:user)
    }
  end
end

# Usage in tests
user = insert(:user)
admin = insert(:admin)
post = insert(:post, author: user)
```

## StreamData Property Testing

```elixir
defmodule MyApp.ValidatorTest do
  use ExUnit.Case, async: true
  use ExUnitProperties

  property "email validator accepts all valid emails" do
    check all local <- string(:alphanumeric, min_length: 1),
              domain <- string(:alphanumeric, min_length: 1) do
      email = "#{local}@#{domain}.com"
      assert Validator.valid_email?(email)
    end
  end

  property "encode then decode is identity" do
    check all data <- binary() do
      assert Base.decode64!(Base.encode64(data)) == data
    end
  end
end
```

## Testing GenServers

```elixir
defmodule MyApp.CacheTest do
  use ExUnit.Case, async: true

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

  test "stores and retrieves values" do
    MyApp.Cache.put(:key, "value")
    assert MyApp.Cache.get(:key) == "value"
  end

  test "returns nil for missing keys" do
    assert MyApp.Cache.get(:missing) == nil
  end
end
```

## Anti-Patterns

```elixir
# WRONG: Testing implementation, not behavior
test "calls Repo.insert" do
  expect(MockRepo, :insert, fn _ -> {:ok, %User{}} end)
  Users.create(%{email: "test@example.com"})
  verify!(MockRepo)
end

# CORRECT: Test observable behavior
test "creates user in database" do
  assert {:ok, user} = Users.create(%{email: "test@example.com"})
  assert Repo.get(User, user.id)
end

# WRONG: Sleeping for async operations
test "sends email asynchronously" do
  Users.register(%{email: "test@example.com"})
  Process.sleep(100)  # Flaky!
  assert email_sent?()
end

# CORRECT: Use ExUnit's built-in async testing patterns
test "enqueues email job" do
  assert {:ok, _} = Users.register(%{email: "test@example.com"})
  assert_enqueued worker: WelcomeEmailWorker, args: %{email: "test@example.com"}
end
```

## Reference

- See `elixir-patterns` skill for GenServer, Supervisor, and Phoenix context patterns
- See `rules/elixir/testing.md` for project-level testing standards
