Structure context tests with `describe` blocks

almirsarajcic

almirsarajcic

7 hours ago

0 comments

Phoenix context test files grow fast. A single accounts_test.exs can easily reach 300+ lines, and without structure it becomes hard to find tests, spot missing coverage, or understand what a function is supposed to do.

ExUnit.Case has a describe/2 macro built in. Use it to mirror your context’s public API:

defmodule MyApp.AccountsTest do
  use MyApp.DataCase, async: true

  alias MyApp.Accounts

  describe "get_user/1" do
    test "returns the user when found" do
      user = user_fixture()
      assert Accounts.get_user(user.id) == user
    end

    test "returns nil when not found" do
      refute Accounts.get_user(-1)
    end
  end

  describe "create_user/1" do
    test "creates a user with valid attributes" do
      assert {:ok, user} = Accounts.create_user(%{email: "alice@example.com"})
      assert user.email == "alice@example.com"
    end

    test "returns an error changeset with invalid attributes" do
      assert {:error, %Ecto.Changeset{}} = Accounts.create_user(%{email: nil})
    end
  end

  describe "delete_user/1" do
    test "deletes the user" do
      user = user_fixture()
      assert {:ok, _} = Accounts.delete_user(user)
      refute Accounts.get_user(user.id)
    end
  end
end

Each describe block becomes its own named scope. Test names in output include the block name, so failures read as "create_user/1 returns an error changeset with invalid attributes" — immediately actionable.

A few practical benefits:

  • Coverage gaps become obvious — if describe "update_user/1" doesn’t exist, you know it’s untested
  • Setup is scoped — use setup inside a describe block and it only runs for those tests
  • Tags apply to the group@describetag :integration skips or includes the whole block at once
describe "send_welcome_email/1" do
  @describetag :integration

  test "sends an email to the user" do
    user = user_fixture()
    assert {:ok, _} = Accounts.send_welcome_email(user)
  end
end

One describe block per public function is a good default. It won’t stay perfectly organised forever, but it gives every test a home and makes the file scannable at a glance.

Comments (0)

Sign in with GitHub to join the discussion