Skip to content
ExUnit Testing
step 1/7

Reading — step 1 of 7

Learn

~3 min readMix and Testing

Elixir comes with a first-class test framework: ExUnit. The Elixir docs treat it as foundational; every Mix project gets a test/ directory by default.

Note: Judge0 runs scripts directly, not Mix tests. This lesson is conceptual.

Test file structure

A Mix project comes with test/:

# test/my_app_test.exs
defmodule MyAppTest do
    use ExUnit.Case
    doctest MyApp
    
    test "greets the world" do
        assert MyApp.hello() == :world
    end
end
  • use ExUnit.Case — gives you test, assert, refute, etc.
  • test "description" do ... end — declares a test
  • assert — checks a condition; fails the test on falsy

Common assertions

assert 1 + 1 == 2
refute 1 + 1 == 3                 # opposite — passes if false

assert_raise ArgumentError, fn ->
    String.to_integer("abc")
end

assert_in_delta 0.1 + 0.2, 0.3, 0.001    # float comparison with tolerance

assert_received :hello             # process received this message
assert_receive :hello, 100         # waits up to 100ms

ExUnit's assert is special — it shows a structured diff on failure. assert {a, b} == {1, 2} reports which field differed and how.

Pattern matching in assert

test "returns ok tuple" do
    assert {:ok, value} = MyModule.parse("42")
    assert value == 42
end

The = is the match operator. If MyModule.parse returns something other than {:ok, _}, the test fails. If it matches, value is bound for further assertions.

Setup

defmodule UserTest do
    use ExUnit.Case
    
    setup do
        # runs before EVERY test
        user = %User{name: "Ada", age: 36}
        {:ok, user: user}                  # named context, passed to tests
    end
    
    test "name is set", %{user: user} do
        assert user.name == "Ada"
    end
    
    test "age is set", %{user: user} do
        assert user.age == 36
    end
end

setup runs before each test; the returned tuple's second element is destructured into the test's context.

setup_all runs ONCE before all tests in the module — for expensive shared state.

describe — grouping

defmodule MathTest do
    use ExUnit.Case
    
    describe "add/2" do
        test "two positives", do: assert Math.add(2, 3) == 5
        test "with zero", do: assert Math.add(0, 5) == 5
    end
    
    describe "divide/2" do
        test "normal", do: assert Math.divide(10, 2) == 5
        test "by zero raises" do
            assert_raise ArithmeticError, fn -> Math.divide(10, 0) end
        end
    end
end

Grouping organizes related tests. Output shows them under their describe block.

Mocking — explicit, not magic

Elixir doesn't auto-mock. Patterns:

Behaviour-based — pass a module:

def MyApp.do_work(notifier \\ MyApp.RealNotifier) do
    notifier.send("done")
end

# In test:
defmodule FakeNotifier do
    @behaviour MyApp.Notifier
    def send(msg), do: send(self(), {:notified, msg})
end

test "notifies on completion" do
    MyApp.do_work(FakeNotifier)
    assert_received {:notified, "done"}
end

Library Mox is the standard for behaviour-based mocking in production projects. Less magic than mocha (Ruby) or sinon (JS).

Async tests

use ExUnit.Case, async: true

Allows tests in this module to run concurrently with tests in OTHER modules. Big speedup for I/O-bound test suites. Don't use if your tests share global state (DB without sandboxing, etc.).

Doctests

Documentation doubles as tests:

defmodule Math do
    @doc """
    Adds two numbers.
    
    ## Examples
    
        iex> Math.add(2, 3)
        5
        
        iex> Math.add(-1, 1)
        0
    """
    def add(a, b), do: a + b
end

# In tests/math_test.exs:
defmodule MathTest do
    use ExUnit.Case
    doctest Math               # extracts and runs all iex examples
end

Examples in ## Examples sections are extracted as tests. Documentation that can't lie because it's tested.

Common mistakes

  • Forgetting use ExUnit.Case — test functions don't get registered.
  • Tests with side effects on global state without async: false — flaky concurrent tests.
  • Not using assert with pattern matching — manual case in tests is verbose.
  • Skipping doctests — examples in docs go stale fast. doctest validates.
  • Mocking too aggressively — explicit fakes via behaviours are cleaner than dynamic mocks. Less magic, easier to reason about.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…