Skip to content

Instantly share code, notes, and snippets.

@seancribbs
Last active February 14, 2024 17:30
Show Gist options
  • Save seancribbs/9797526 to your computer and use it in GitHub Desktop.
Save seancribbs/9797526 to your computer and use it in GitHub Desktop.
Presentation source, code examples and exercises from "Getting Started with eunit", Chicago Erlang March 2014

Getting Started with eunit

1 Introduction

1.1 Why use eunit?

  • Simple to get started
  • Familiar if you’ve written unit-tests in other languages
  • Long runway, good tool integration

2 Getting Started

2.1 Boilerplate/Setup

  • Have complete Erlang/OTP installed (some distros have erlang-eunit as a package)
  • Create a directory to work in
  • Create a module to hold tests
-module(mytests).
-compile([export_all, debug_info]).
-include_lib("eunit/include/eunit.hrl").

2.2 Assert True

  • All test functions are suffixed with _test or _test_.
  • Assertions are macros included from eunit.hrl
  • Create a simple test that asserts true:
truth_test() ->
    ?assert(true).

2.3 Run the Test

  • Start an Erlang shell
$ erl
Eshell V5.10.3  (abort with ^G)
  • Compile the test module
1> c(mytests).
{ok,mytests}
  • Run eunit on the module
2> eunit:test(mytests). %% or mytests:test().
   Test passed.
ok

2.4 Failing Assertion

  • Make the test fail!
truth_test() ->
    ?assert(false).
  • Run eunit
3> c(mytests), eunit:test(mytests).
mytests: truth_test (module 'mytests')...*failed*
in function mytests:'-truth_test/0-fun-0-'/0 (mytests.erl, line 6)
**error:{assertion_failed,[{module,mytests},
                   {line,6},
                   {expression,"false"},
                   {expected,true},
                   {value,false}]}

2.5 Assertions

  • Simple truth, falsehood
?assert( TruthValue )
?assertNot( FalseValue )
  • Equality, inequality
?assertEqual( Expected, Expression )
?assertNotEqual( Unexpected, Expression )
  • Pattern matching
?assertMatch( Pattern, Expression )
?assertNotMatch( Pattern, Expression )

2.6 More Assertions

  • Exceptions
?assertException( Class, Term, Expression )
?assertNotException( Class, Term, Expression )
   %% Class = exit | error | throw
?assertError( Term, Expression )
?assertExit( Term, Expression )
?assertThrow( Term, Expression )
  • External commands
?cmd( ShellCommand )
?assertCmdStatus( ExitStatus, ShellCommand )
?assertCmdOutput( Text, ShellCommand )

3 Testing a Simple Module

3.1 Testing a Simple Module

3.1.1 Learning by example

Let’s test a naive implementation of a familiar data structure, a stack. In Erlang we will represent these as lists, wrapped in a module that implements New, Push, Pop and Size operations.

3.2 Stack module

-module(stack).
-export([new/0, push/2, pop/1, size/1]).

:

new() -> [].

:

push(Thing, Stack) -> [Thing|Stack].

:

pop(Stack) -> {hd(Stack), tl(Stack)}.

:

size(Stack) -> length(Stack).

3.3 Review: Setup eunit

  • Create stack_tests module
  • Include eunit.hrl
  • Compile both stack and stack_tests in the shell

3.4 Things we could test

  • New stacks are empty (size 0)
  • Push followed by Pop returns original item and stack
  • Push increases size of stack
  • Pop decreases size of stack
  • Pop on an empty stack raises an error

3.5 Pop empty stack

pop_empty_raises_error_test() ->
    ?assertError(badarg,
                 stack:pop(stack:new())).

3.6 Improving output

16> eunit:test(stack_tests, [verbose]).
======================== EUnit ========================
module 'stack_tests'
  stack_tests: new_stacks_are_empty_test...ok
  stack_tests: lifo_test...ok
  stack_tests: push_increases_size_test...ok
  stack_tests: pop_decreases_size_test...ok
  stack_tests: pop_empty_raises_error_test...ok
  [done in 0.014 s]
=======================================================
  All 5 tests passed.

See also github.com/seancribbs/eunit_formatters

4 Advanced Test Suites

4.1 Advanced Test Suites

Things you might want to do:

  • Integrate the test suite with build tools
  • Perform setup/teardown around tests
  • Enforce timeouts on tests that might stall
  • Group multiple related tests together more tightly
  • Debug state and progress inside a test
  • Report on code coverage of your tests

4.2 Build tools

4.2.1 rebar

  • Run rebar eunit
  • Defines TEST macro
  • Includes modules in test/ directory when testing

4.2.2 erlang.mk

Uses common\_test only, you have to call eunit manually in your ct suite.

4.3 Setup and Teardown

Sometimes you need to do setup or teardown around tests. You can group together tests that require the same environment:

similar_tests_() ->                    % use trailing _!
   {setup,
    fun() -> start_server() end,       % setup
    fun(Pid) -> stop_server(Pid) end,  % cleanup
    [
      %% {TestDescription, TestFunction}
      {"test 1", fun test1/0},
      {"test 2", fun test2/0}
    ]}.

You can also use foreach instead of setup to run setup/cleanup around every test.

4.4 Timeouts

eunit runs every test in a new process and kills it after 5 seconds by default. You can increase/decrease the timeout like so:

slow_test_() ->           % use trailing _!
    {timeout,
     60,                  % 1 minute, in seconds
     [fun slow_thing/0]}. % can be a list of tests

4.5 Grouping tests

Sometimes you want to treat a bunch of tests as a group, in which case you can just return a list of them. Note an assertion with the underscore prefix creates a test function that wraps the assertion.

fizzbuzz_test_() ->       % use trailing _!
    [ ?_assertEqual(fizz, fizzbuzz(3)),
      ?_assertEqual(buzz, fizzbuzz(5)),
      ?_assertEqual(fizzbuzz, fizzbuzz(15)),
      ?_assertEqual(7, fizzbuzz(7)) ].

4.6 Debugging

It can be hard to tell what’s going on in complex tests. Debug macros help.

  • ?debugMsg("Trying fizzbuzz")
    mytests.erl:62:<0.126.0> Trying fizzbuzz
        
  • ?debugHere
    mytests.erl:65:<0.126.0> <-
        
  • ?debugVal(Count)
    mytests.erl:71:<0.126.0> Count = 10
        
  • ?debugFmt("Got ~p", [A])
    mytests.erl:83:<0.126.0> Got fizz
        
  • ?debugTime("fizz", netfizz())
    mytests.erl:100:<0.126.0> fizzing: 0.321 s
        

4.7 Code coverage

  • rebar supports computing and outputting coverage automatically. Add this line to rebar.config:
    {cover_enabled, true}.
        

    After running rebar eunit, open .eunit/index.html

  • Otherwise:
    cover:start().
    cover:compile_beam_directory(".").
    eunit:test(mytests).
    [cover:analyze_to_file(Mod, [html])
        || Mod <- cover:modules()].
    cover:stop().
        

5 Gotchas and Pitfalls

5.1 Gotchas and Pitfalls

  • eunit swallows console output by default. Write to log files or use ?debug macros.
  • Ensure your tests are pure or cleanup properly after themselves. Ordering issues can plague test suites run on different machines.
  • Complicated setup/teardown combos can be hard to reason about. Move to common\_test if your suite becomes hard to setup.
  • Know when to use the _ on test functions:
    • If your function executes directly and uses regular assertions, omit the underscore.
    • If your function uses fixtures or returns a list of tests, include the underscore.

5.2 Resources

-module(mytests).
-compile([export_all, debug_info]).
-include_lib("eunit/include/eunit.hrl").
truth_test() ->
?assert(foo).
-module(stack).
-export([new/0, push/2, pop/1, size/1]).
new() -> [].
push(Thing, Stack) -> [Thing|Stack].
pop(Stack) -> {hd(Stack), tl(Stack)}.
size(Stack) -> length(Stack).
-module(stack_tests).
-compile([export_all, debug_info]).
-include_lib("eunit/include/eunit.hrl").
new_stacks_are_empty_test() ->
?assertEqual(0, stack:size(stack:new())).
lifo_test() ->
Stack0 = stack:new(),
Stack1 = stack:push(a, Stack0),
?assertMatch({a, _}, stack:pop(Stack1)).
push_increases_size_test() ->
Stack0 = stack:new(),
Stack1 = stack:push(a, Stack0),
?assert(stack:size(Stack0) < stack:size(Stack1)).
pop_decreases_size_test() ->
Stack0 = stack:push(a, stack:push(b, stack:new())),
{a, Stack1} = stack:pop(Stack0),
?assert(stack:size(Stack0) > stack:size(Stack1)).
pop_empty_raises_error_test() ->
?assertError(badarg, stack:pop(stack:new())).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment