Reading — step 1 of 7
Learn
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 youtest,assert,refute, etc.test "description" do ... end— declares a testassert— 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
casein 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…