100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Testing Erlang with EUnit

Learn how to write, organize, and run unit tests for Erlang code using the built-in EUnit framework.

Practical ErlangBeginner8 min readJul 10, 2026
Analogies

What EUnit Is and How Tests Are Discovered

EUnit is Erlang's built-in unit testing framework, shipped as part of standard OTP, so no external dependency is required to start writing tests — just add -include_lib("eunit/include/eunit.hrl"). A function whose name ends in _test/0 is automatically discovered and run as a plain test when you call eunit:test(Module) or rebar3 eunit. Test modules conventionally live in a project's test/ directory and are named after the module they exercise, such as calc_tests.erl testing calc.erl.

🏏

Cricket analogy: EUnit shipping as part of core OTP rather than a separate download is like the DRS review system being built into every ICC-sanctioned match rather than something a team has to bring themselves — it's simply there and ready to use.

Writing Assertions

EUnit provides a family of assertion macros: ?assert(Bool), ?assertEqual(Expected, Actual), ?assertMatch(Pattern, Expr), ?assertError(Reason, Expr), and ?assertException(Class, Reason, Expr) among others. Each macro expands to code that, on failure, raises an informative error term including the expected value, the actual value, the module, and the exact line number — far more useful for debugging than a bare if statement that only tells you something was wrong.

🏏

Cricket analogy: ?assertEqual reporting both the expected and actual value on failure is like a third umpire's review showing both the batter's claimed and the ball-tracking predicted trajectory side by side, rather than just saying 'out' with no explanation.

erlang
-module(calc_tests).
-include_lib("eunit/include/eunit.hrl").

%% Plain test: runs once, no setup needed
add_test() ->
    ?assertEqual(4, calc:add(2, 2)),
    ?assertEqual(-1, calc:add(2, -3)).

%% Test generator using a fixture to start/stop a process around each test
server_test_() ->
    {setup,
     fun() -> {ok, Pid} = calc_server:start_link(), Pid end,
     fun(Pid) -> calc_server:stop(Pid) end,
     fun(Pid) ->
         [?_assertEqual(0, calc_server:value(Pid)),
          ?_assertEqual(5, begin calc_server:add(Pid, 5), calc_server:value(Pid) end)]
     end}.

%% Checking that a bad argument raises the expected error
bad_input_test() ->
    ?assertError(badarith, calc:add(2, not_a_number)).

Test Generators and Fixtures

A function whose name ends in _test_() (note the extra trailing underscore) is a test generator, expected to return a fun, tuple, or list describing tests rather than performing assertions directly. This lets you build fixtures like {setup, Setup, Cleanup, Tests} or {foreach, Setup, Cleanup, [Tests]}, where Setup runs once (or before each test for foreach) to prepare shared state — such as starting a gen_server — and Cleanup guarantees teardown afterward, even if a test fails. Inside a generator, use the underscore-prefixed macros like ?_assertEqual, which produce deferred test objects instead of asserting immediately.

🏏

Cricket analogy: A {setup, Setup, Cleanup, Tests} fixture is like a ground staff preparing the exact same pitch conditions before every net session and rolling it flat again afterward, so each batter practices against a consistent, freshly reset surface.

rebar3 eunit runs every *_test/0 and *_test_/0 function it finds under test/ (and inline in src/ if -ifdef(TEST) blocks are used); pass rebar3 eunit --module=calc_tests to run a single suite, and follow with rebar3 cover --verbose to see which lines your tests actually exercised.

Common Pitfalls

The single most common EUnit mistake is confusing the plain _test suffix with the generator _test_ suffix — using the wrong one means EUnit either treats a generator function as a plain test (and fails oddly on its return value) or fails to collect the tests a generator was supposed to produce. Another frequent issue is leaking real OTP processes started inside a test without cleaning them up in a fixture's Cleanup function, which causes flaky, order-dependent failures as stray processes from one test interfere with the next.

🏏

Cricket analogy: Forgetting the trailing underscore that turns a plain _test into a _test_ generator is like a scorer misreading 'not out' as 'out' from similar-looking handwriting — one tiny character changes how the whole entry is interpreted by everyone downstream.

  • EUnit ships as part of standard OTP — no external dependency is needed to write and run unit tests.
  • A function named foo_test/0 is a plain test; a function named foo_test_/0 is a test generator returning a fun, tuple, or list of tests.
  • Assertion macros like ?assertEqual, ?assertMatch, and ?assertError report both expected and actual values on failure for easier debugging.
  • {setup, Setup, Cleanup, Tests} and {foreach, Setup, Cleanup, [Tests]} fixtures let tests share consistent setup/teardown logic, such as starting and stopping a gen_server.
  • rebar3 eunit is the standard command to run a project's EUnit suite; --module=Name scopes it to one test module.
  • Test generators use ?_assert-family macros (leading underscore) instead of ?assert to defer execution until EUnit collects the generated tests.
  • Leaked processes from tests that don't clean up in a fixture's Cleanup function are a common source of flaky, order-dependent test failures.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ErlangStudyNotes#TestingErlangWithEUnit#Testing#Erlang#EUnit#Tests#StudyNotes#SkillVeris#ExamPrep