Everything you always wanted to know about testing on the BEAM

Test Driven Development

A software development practice that emphasizes writing tests before writing the actual code

Development

Test

Development

Test

Driven

Development

Test

Driven

Development

Stage 1:

Learning

Using TDD to Learn Erlang

# PRESENTING CODE

The Emojifier

1> {ok, Emojifier0} = emojifier:start().
…
2> {ok, Emojifier1} = emojifier:add("(tree)", "🌳").
…
3> emojifier:emojify(Emojifier1, "This string has a tree: (tree)").
"This string has a tree: 🌳"
4> emojifier:emojify(
4>          Emojifier1, "This string doesn't have a tree: :tree:").
"This string doesn't have a tree: :tree:"
5>

No relation with beamoji at all 🙄

# PRESENTING CODE

Create a Module

-module emojifier.
emojifier.erl
# PRESENTING CODE

Add test/0

-module emojifier.

-export [test/0].

test() ->
	…
    '✅'.
emojifier.erl
# PRESENTING CODE

Write the First Test

-module emojifier.

-export [test/0].

test() ->
    {ok, _} = emojifier:start(),
    '✅'.
emojifier.erl
# PRESENTING CODE

Run the Tests

1> {ok, emojifier} = c(emojifier), emojifier:test().
** exception error: undefined function emojifier:start/0
     in function  emojifier:test/0 (emojifier.erl, line 6)
2>
# PRESENTING CODE

Make it work

-module emojifier.

-export [test/0].
-export [start/0].

test() ->
    {ok, _} = emojifier:start(),
    '✅'.
    
start() -> {ok, #{}}.
emojifier.erl
# PRESENTING CODE

Run the Tests

1> {ok, emojifier} = c(emojifier), emojifier:test().
'✅'
2>
# PRESENTING CODE

Add More Tests!

-module emojifier.

-export [test/0].
-export [start/0].

test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    '✅'.
    
start() -> {ok, #{}}.
emojifier.erl
# PRESENTING CODE

Run the Tests

1> {ok, emojifier} = c(emojifier), emojifier:test().
** exception error: undefined function emojifier:emojify/2
     in function  emojifier:test/0 (emojifier.erl, line 8)
2>
# PRESENTING CODE

Make it work

-module emojifier.

-export [test/0].
-export [start/0].

test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    '✅'.
    
start() -> {ok, #{}}.
emojifier.erl
-module emojifier.

-export [test/0].
-export [start/0, emojify/2].

test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    '✅'.
    
start() -> {ok, #{}}.

emojify(_, String) -> String.
emojifier.erl
# PRESENTING CODE

Run the Tests

1> {ok, emojifier} = c(emojifier), emojifier:test().
'✅'
2>
# PRESENTING CODE

Add More Tests!

-module emojifier.

-export [test/0].
-export [start/0, emojify/2].

test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    '✅'.
    
start() -> {ok, #{}}.

emojify(_, String) -> String.
emojifier.erl
-module emojifier.

-export [test/0].
-export [start/0, emojify/2].

test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    Emojifier1 = emojifier:add(Emojifier0, ":smile:", "😊"),
    "With 😊" = emojifier:emojify(Emojifier1, "With :smile:"),
    "No :smile" = emojifier:emojify(Emojifier1, "No :smile"),
    '✅'.
    
start() -> {ok, #{}}.

emojify(_, String) -> String.
emojifier.erl
# PRESENTING CODE

Run the Tests

1> {ok, emojifier} = c(emojifier), emojifier:test().
** exception error: undefined function emojifier:add/3
     in function  emojifier:test/0 (emojifier.erl, line 10)
2>
# PRESENTING CODE

Add More Tests!

-module emojifier.

-export [test/0].
-export [start/0, emojify/2, add/3].

test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    Emojifier1 = emojifier:add(Emojifier0, ":smile:", "😊"),
    "With 😊" = emojifier:emojify(Emojifier1, "With :smile:"),
    ":smile" = emojifier:emojify(Emojifier1, "No :smile"),
    '✅'.
    
start() -> {ok, #{}}.

emojify(_, String) -> String.

add(_, Text, Emoji) -> #{Text => Emoji}.
emojifier.erl
# PRESENTING CODE

Run the Tests

1> {ok, emojifier} = c(emojifier), emojifier:test().
** exception error: no match of right hand side value "With :smile:"
     in function  emojifier:test/0 (emojifier.erl, line 11)
2>
# PRESENTING CODE

Make it work

-module emojifier.

-export [test/0].
-export [start/0, emojify/2, add/3].

test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    Emojifier1 = emojifier:add(Emojifier0, ":smile:", "😊"),
    "With 😊" = emojifier:emojify(Emojifier1, "With :smile:"),
    "No :smile" = emojifier:emojify(Emojifier1, "No :smile"),
    '✅'.
    
start() -> {ok, #{}}.

emojify(_, String) -> String.

add(_, Text, Emoji) -> #{Text => Emoji}.
emojifier.erl

test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    Emojifier1 = emojifier:add(Emojifier0, ":smile:", "😊"),
    "With 😊" = emojifier:emojify(Emojifier1, "With :smile:"),
    "No :smile" = emojifier:emojify(Emojifier1, "No :smile"),
    '✅'.

start() -> {ok, #{}}.

emojify(Emojis, String) ->
    maps:fold(
        fun(Text, Emoji, AccString) ->
            string:replace(AccString, Text, Emoji)
        end, String, Emojis).

add(_, Text, Emoji) -> #{Text => Emoji}.
# PRESENTING CODE

Run the Tests

1> {ok, emojifier} = c(emojifier), emojifier:test().
** exception error: no match of right hand side value ["With ",[128522],[]]
     in function  emojifier:test/0 (emojifier.erl, line 11)
2>
# PRESENTING CODE

Make it work


test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    Emojifier1 = emojifier:add(Emojifier0, ":smile:", "😊"),
    "With 😊" = emojifier:emojify(Emojifier1, "With :smile:"),
    "No :smile" = emojifier:emojify(Emojifier1, "No :smile"),
    '✅'.

start() -> {ok, #{}}.

emojify(Emojis, String) ->
    maps:fold(
        fun(Text, Emoji, AccString) ->
            string:replace(AccString, Text, Emoji)
        end, String, Emojis).

add(_, Text, Emoji) -> #{Text => Emoji}.
emojifier.erl

test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    Emojifier1 = emojifier:add(Emojifier0, ":smile:", "😊"),
    "With 😊" = emojifier:emojify(Emojifier1, "With :smile:"),
    "No :smile" = emojifier:emojify(Emojifier1, "No :smile"),
    '✅'.

start() -> {ok, #{}}.

emojify(Emojis, String) ->
    lists:flatten(maps:fold(
        fun(Text, Emoji, AccString) ->
            string:replace(AccString, Text, Emoji)
        end, String, Emojis)).

add(_, Text, Emoji) -> #{Text => Emoji}.
emojifier.erl
# PRESENTING CODE

Run the Tests

1> {ok, emojifier} = c(emojifier), emojifier:test().
'✅'
2>
# PRESENTING CODE

Add Even More Tests!


test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    Emojifier1 = emojifier:add(Emojifier0, ":smile:", "😊"),
    "With 😊" = emojifier:emojify(Emojifier1, "With :smile:"),
    "No :smile" = emojifier:emojify(Emojifier1, "No :smile"),
    '✅'.

start() -> {ok, #{}}.

emojify(Emojis, String) ->
    lists:flatten(maps:fold(
        fun(Text, Emoji, AccString) ->
            string:replace(AccString, Text, Emoji)
        end, String, Emojis)).

add(_, Text, Emoji) -> #{Text => Emoji}.
emojifier.erl
# PRESENTING CODE

Final version of the Tests


test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    Emojifier1 = emojifier:add(Emojifier0, ":smile:", "😊"),
    "With 😊" = emojifier:emojify(Emojifier1, "With :smile:"),
    "No :smile" = emojifier:emojify(Emojifier1, "No :smile"),
    Emojifier2 = emojifier:add(Emojifier1, ":mate:", "🧉"),
    "With 😊 and 🧉" =
        emojifier:emojify(Emojifier2, "With :smile: and :mate:"),
    "With 2 🧉🧉" =
        emojifier:emojify(Emojifier2, "With 2 :mate::mate:"),
    '✅'.
emojifier.erl

start() -> {ok, #{}}.

emojify(Emojis, String) ->
    lists:flatten(maps:fold(
        fun(Text, Emoji, AccString) ->
            string:replace(AccString, Text, Emoji, all)
        end, String, Emojis)).

add(Emojis, Text, Emoji) -> Emojis#{Text => Emoji}.
emojifier.erl
# PRESENTING CODE

Run the Tests

1> {ok, emojifier} = c(emojifier), emojifier:test().
'✅'
2>

Stage 2:

Learning OTP

Using TDD with OTP Behaviours

# PRESENTING CODE

The Tests


test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    Emojifier1 = emojifier:add(Emojifier0, ":smile:", "😊"),
    "With 😊" = emojifier:emojify(Emojifier1, "With :smile:"),
    "No :smile" = emojifier:emojify(Emojifier1, "No :smile"),
    Emojifier2 = emojifier:add(Emojifier1, ":mate:", "🧉"),
    "With 😊 and 🧉" =
        emojifier:emojify(Emojifier2, "With :smile: and :mate:"),
    "With 2 🧉🧉" =
        emojifier:emojify(Emojifier2, "With 2 :mate::mate:"),
    '✅'.
emojifier.erl

test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    Emojifier1 = emojifier:add(Emojifier0, ":smile:", "😊"),
    "With 😊" = emojifier:emojify(Emojifier1, "With :smile:"),
    "No :smile" = emojifier:emojify(Emojifier1, "No :smile"),
    Emojifier2 = emojifier:add(Emojifier1, ":mate:", "🧉"),
    "With 😊 and 🧉" =
        emojifier:emojify(Emojifier2, "With :smile: and :mate:"),
    "With 2 🧉🧉" =
        emojifier:emojify(Emojifier2, "With 2 :mate::mate:"),
    '✅'.
emojifier.erl
# PRESENTING CODE

The Code

start() -> {ok, #{}}.

emojify(Emojis, String) ->
    lists:flatten(maps:fold(
        fun(Text, Emoji, AccString) ->
            string:replace(AccString, Text, Emoji, all)
        end, String, Emojis)).

add(Emojis, Text, Emoji) -> Emojis#{Text => Emoji}.
emojifier.erl
start() -> gen_server:start(emojifier, noargs, []).

init(noargs) -> {ok, #{}}.

emojify(Emojifier, String) ->
    gen_server:call(Emojifier, {emojify, String}).

handle_call({emojify, String}, _From, Emojis) ->
    {reply,
     lists:flatten(maps:fold(
        fun(Text, Emoji, AccString) ->
            string:replace(AccString, Text, Emoji, all)
        end, String, Emojis)),
     Emojis}.

add(Emojifier, Text, Emoji) ->
    gen_server:cast(Emojifier, {add, Text, Emoji}), Emojifier.

handle_cast({add, Text, Emoji}, Emojis) ->
    {noreply, Emojis#{Text => Emoji}}.
emojifier.erl
# PRESENTING CODE

Run the Tests

1> {ok, emojifier} = c(emojifier), emojifier:test().
'✅'
2>
# PRESENTING CODE

Run the Tests

1> {ok, emojifier} = c(emojifier, [debug]), emojifier:test().
emojifier.erl:7:2:
	Warning: undefined callback function handle_info/2
    		 (behaviour 'gen_server')
'✅'
2>

The Tradeoff

3.

Test the Callback

Pros:

  • 100% test coverage
  • TDD respect

Cons: Test ≄ Design

# Tradeoff

Leave the Warning

Pros:

  • 100% test coverage
  • TDD respect

Cons: Warnings.

1.

2.

Implement Callback

Pros: No Warnings

Cons:

  • < 100% test coverage
  • TDD disrespect
# PRESENTING CODE

Add the Dumb Test


test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    Emojifier1 = emojifier:add(Emojifier0, ":smile:", "😊"),
    "With 😊" = emojifier:emojify(Emojifier1, "With :smile:"),
    "No :smile" = emojifier:emojify(Emojifier1, "No :smile"),
    Emojifier2 = emojifier:add(Emojifier1, ":mate:", "🧉"),
    "With 😊 and 🧉" =
        emojifier:emojify(Emojifier2, "With :smile: and :mate:"),
    "With 2 🧉🧉" =
        emojifier:emojify(Emojifier2, "With 2 :mate::mate:"),
    '✅'.
emojifier.erl

test() ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    Emojifier1 = emojifier:add(Emojifier0, ":smile:", "😊"),
    "With 😊" = emojifier:emojify(Emojifier1, "With :smile:"),
    "No :smile" = emojifier:emojify(Emojifier1, "No :smile"),
    Emojifier2 = emojifier:add(Emojifier1, ":mate:", "🧉"),
    "With 😊 and 🧉" =
        emojifier:emojify(Emojifier2, "With :smile: and :mate:"),
    "With 2 🧉🧉" =
        emojifier:emojify(Emojifier2, "With 2 :mate::mate:"),
    complete_coverage(Emojifier2),
    '✅'.

complete_coverage(Emojifier) ->
    Emojifier ! {an, unexpected, messsage, '😱'}.
   
emojifier.erl
# PRESENTING CODE

Run the Tests

1> {ok, emojifier} = c(emojifier, [debug]), emojifier:test().
'✅'
=WARNING REPORT==== 26-Sep-2023::08:36:04.081868 ===
** Undefined handle_info in emojifier
** Unhandled message: {an,unexpected,messsage,'😱'}
2>
# PRESENTING CODE

The Code

start() -> gen_server:start(emojifier, noargs, []).
init(noargs) -> {ok, #{}}.

emojify(Emojifier, String) ->
    gen_server:call(Emojifier, {emojify, String}).
handle_call({emojify, String}, _From, Emojis) ->
    {reply,
     lists:flatten(maps:fold(
        fun(Text, Emoji, AccString) ->
            string:replace(AccString, Text, Emoji, all)
        end, String, Emojis)),
     Emojis}.

add(Emojifier, Text, Emoji) ->
    gen_server:cast(Emojifier, {add, Text, Emoji}), Emojifier.
handle_cast({add, Text, Emoji}, Emojis) ->
    {noreply, Emojis#{Text => Emoji}}.

handle_info(_Msg, Emojis) -> {noreply, Emojis}.
emojifier.erl
# PRESENTING CODE

Run the Tests

1> {ok, emojifier} = c(emojifier), emojifier:test().
'✅'
2>

Stage 3:

Learning rebar3

Using TDD with rebar3 and CT

# PRESENTING CODE

Project Configuration

{erl_opts,
 [warn_unused_import, warn_export_vars, warnings_as_errors,
  verbose, report, debug_info]}.

{minimum_otp_vsn, "24"}.

{alias,
 [{test,
   [compile, format, hank, lint, xref,
    dialyzer, ct, cover, {covertool, "generate"},
    ex_doc]}]}.

{project_plugins,
 [{rebar3_hank, "~> 1.4.0"},
  {rebar3_hex, "~> 7.0.7"},
  {rebar3_format, "~> 1.3.0"},
  {rebar3_lint, "~> 3.0.1"},
  {rebar3_ex_doc, "0.2.18"},
  {rebar3_depup, "~> 0.3.1"},
  {covertool, "~> 2.0.6"}]}.
rebar.config
%% This page was intentionally left blank 🧐
rebar.config
# PRESENTING CODE

App Configuration

{application,
 emojifier,
 [{description, "Add magic to your strings 🧙‍♂️"},
  {vsn, git},
  {id, "emojifier"},
  {applications, [kernel, stdlib]},
  {mod, {emojifier, noargs}}]}.
src/emojifier.app.src
# PRESENTING CODE

Run the Tests

$ rebar3 ct
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling emojifier
===> Running Common Test suites...
All 0 tests passed.
$
# PRESENTING CODE

Our first Test Suite

-module emojifier_SUITE.

-behaviour ct_suite.

-export [all/0].
-export [app/1].

all() -> [app].

app(_) ->
    {ok, [emojifier]} = application:ensure_all_started(emojifier),
    ok = application:stop(emojifier).
test/emojifier_SUITE.erl
# PRESENTING CODE

Run the Tests

$ rebar3 ct
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling emojifier
===> Running Common Test suites...
%%% emojifier_SUITE:
=CRASH REPORT==== 28-Sep-2023::10:41:42.852472 ===
  crasher:
%  …a lot of data about the crash…
=INFO REPORT==== 28-Sep-2023::10:41:42.852509 ===
    application: emojifier
    exited: {bad_return,
%  …much more info…
%%% emojifier_SUITE ==> app: FAILED
%%% emojifier_SUITE ==> {{badmatch,
    {error,{emojifier,{bad_return,{{emojifier,start,[normal,noargs]},
        {'EXIT',{undef,[{emojifier,start,[normal,noargs],[]},…]}}}}}}},
 [{emojifier_SUITE,app,1,[{file,"test/emojifier_SUITE.erl"},{line,9}]},…]}]}
%  …rebar3 stuff…
Failed 1 tests. Passed 0 tests.
Results written to "_build/test/logs/index.html".
===> Failures occurred running tests: 1
# PRESENTING CODE

The Application Module

-module emojifier.

-behaviour application.

-export [start/2].

start(_, noargs) ->
src/emojifier.erl
-module emojifier.

-behaviour application.

-export [start/2].

start(_, noargs) ->
    emojifier_sup:start_link().
src/emojifier.erl
# PRESENTING CODE

The Supervisor Module

-module emojifier_sup.

-behaviour supervisor.

-export [start_link/0, init/1].

start_link() ->
    supervisor:start_link(
        {local, emojifier_sup}, emojifier_sup, noargs).
    
init(noargs) ->
    {ok, {#{}, []}}.
src/emojifier_sup.erl
# PRESENTING CODE

Run the Tests

$ rebar3 ct
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling emojifier
src/emojifier.erl:3:2: Warning:
	undefined callback function stop/1 (behaviour 'application')

===> Running Common Test suites...
%%% emojifier_SUITE: .
All 1 tests passed.
$
# PRESENTING CODE

Completing Coverage

-module emojifier_SUITE.

-behaviour ct_suite.

-export [all/0].
-export [app/1].

all() -> [app].

app(_) ->
    {ok, [emojifier]} = application:ensure_all_started(emojifier),
    ok = application:stop(emojifier).
test/emojifier_SUITE.erl
-module emojifier_SUITE.

-behaviour ct_suite.

-export [all/0].
-export [app/1, complete_coverage/1].

all() -> [app, complete_coverage].

app(_) ->
    {ok, [emojifier]} = application:ensure_all_started(emojifier),
    ok = application:stop(emojifier).

complete_coverage(_) ->
    _ = emojifier:stop(nostate).
test/emojifier_SUITE.erl
# PRESENTING CODE

Run the Tests

$ rebar3 ct
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling emojifier
===> Running Common Test suites...
%%% emojifier_SUITE: .
%%% emojifier_SUITE ==> complete_coverage: FAILED
%%% emojifier_SUITE ==> {undef,[{emojifier,stop,[nostate],[]},
        {test_server,ts_tc,3,[{file,"test_server.erl"},{line,1783}]},
        {test_server,run_test_case_eval1,6,
                     [{file,"test_server.erl"},{line,1292}]},
        {test_server,run_test_case_eval,9,
                     [{file,"test_server.erl"},{line,1224}]}]}

EXPERIMENTAL: Writing retry specification at _build/test/logs/retry.spec
              call rebar3 ct with '--retry' to re-run failing cases.
Failed 1 tests. Passed 1 tests.
Results written to "_build/test/logs/index.html".
===> Failures occurred running tests: 1
# PRESENTING CODE

Back to The Application Module

-module emojifier.

-behaviour application.

-export [start/2].

start(_, noargs) ->
    emojifier_sup:start_link().
src/emojifier.erl
-module emojifier.

-behaviour application.

-export [start/2, stop/1].

start(_, noargs) ->
    emojifier_sup:start_link().

stop(nostate) -> ok.
src/emojifier.erl
# PRESENTING CODE

Run the Tests

$ rebar3 ct
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling emojifier
===> Running Common Test suites...
%%% emojifier_SUITE: ..
All 2 tests passed.
$
# PRESENTING CODE

The main test

-module emojifier_SUITE.

-behaviour ct_suite.

-export [all/0].
-export [app/1, complete_coverage/1].

all() -> [app, complete_coverage].

app(_) ->
    {ok, [emojifier]} = application:ensure_all_started(emojifier),
    ok = application:stop(emojifier).

complete_coverage(_) ->
    _ = emojifier:stop(nostate).
test/emojifier_SUITE.erl
-module emojifier_SUITE.

-behaviour ct_suite.

-export [all/0].
-export [app/1, complete_coverage/1, emojify/1].

all() -> [app, complete_coverage, emojify].

app(_) ->
    {ok, [emojifier]} = application:ensure_all_started(emojifier),
    ok = application:stop(emojifier).

complete_coverage(_) ->
    _ = emojifier:stop(nostate).
test/emojifier_SUITE.erl
# PRESENTING CODE

The main test

emojify(_) ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    Emojifier1 = emojifier:add(Emojifier0, ":smile:", "😊"),
    "With 😊" = emojifier:emojify(Emojifier1, "With :smile:"),
    "No :smile" = emojifier:emojify(Emojifier1, "No :smile"),
    Emojifier2 = emojifier:add(Emojifier1, ":mate:", "🧉"),
    "With 😊 and 🧉" =
        emojifier:emojify(Emojifier2, "With :smile: and :mate:"),
    "With 2 🧉🧉" =
        emojifier:emojify(Emojifier2, "With 2 :mate::mate:"),
    complete_coverage(Emojifier2),
    '✅'.
test/emojifier_SUITE.erl
emojify(_) ->
    {ok, Emojifier0} = emojifier:start(),
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    Emojifier1 = emojifier:add(Emojifier0, ":smile:", "😊"),
    "With 😊" = emojifier:emojify(Emojifier1, "With :smile:"),
    "No :smile" = emojifier:emojify(Emojifier1, "No :smile"),
    Emojifier2 = emojifier:add(Emojifier1, ":mate:", "🧉"),
    "With 😊 and 🧉" =
        emojifier:emojify(Emojifier2, "With :smile: and :mate:"),
    "With 2 🧉🧉" =
        emojifier:emojify(Emojifier2, "With 2 :mate::mate:"),
    ok.
test/emojifier_SUITE.erl
emojify(_) ->
    "" = emojifier:emojify(Emojifier0, ""),
    "Same String" = emojifier:emojify(Emojifier0, "Same String"),
    Emojifier1 = emojifier:add(Emojifier0, ":smile:", "😊"),
    "With 😊" = emojifier:emojify(Emojifier1, "With :smile:"),
    "No :smile" = emojifier:emojify(Emojifier1, "No :smile"),
    Emojifier2 = emojifier:add(Emojifier1, ":mate:", "🧉"),
    "With 😊 and 🧉" =
        emojifier:emojify(Emojifier2, "With :smile: and :mate:"),
    "With 2 🧉🧉" =
        emojifier:emojify(Emojifier2, "With 2 :mate::mate:"),
    ok.
test/emojifier_SUITE.erl
emojify(_) ->
    "" = emojifier:emojify(""),
    "Same String" = emojifier:emojify("Same String"),
    _ = emojifier:add(":smile:", "😊"),
    "With 😊" = emojifier:emojify("With :smile:"),
    "No :smile" = emojifier:emojify("No :smile"),
    _ = emojifier:add(":mate:", "🧉"),
    "With 😊 and 🧉" =
        emojifier:emojify("With :smile: and :mate:"),
    "With 2 🧉🧉" = emojifier:emojify("With 2 :mate::mate:"),
    ok.
test/emojifier_SUITE.erl
# PRESENTING CODE

The main test

emojify(_) ->
    "" = emojifier:emojify(""),
    "Same String" = emojifier:emojify("Same String"),
    _ = emojifier:add(":smile:", "😊"),
    "With 😊" = emojifier:emojify("With :smile:"),
    "No :smile" = emojifier:emojify("No :smile"),
    _ = emojifier:add(":mate:", "🧉"),
    "With 😊 and 🧉" =emojifier:emojify("With :smile: and :mate:"),
    "With 2 🧉🧉" = emojifier:emojify("With 2 :mate::mate:"),
    ok.

init_per_testcase(emojify, Config) ->
    {ok, _} = application:ensure_all_started(emojifier),
    Config;
init_per_testcase(_, Config) -> Config.

end_per_testcase(emojify, Config) ->
    _ = application:stop(emojifier),
    Config;
end_per_testcase(_, Config) -> Config.
test/emojifier_SUITE.erl
# PRESENTING CODE

Run the Tests

$ rebar3 ct
===> …
===> Running Common Test suites...
%%% emojifier_SUITE: ..
=INFO REPORT==== 3-Oct-2023::11:28:13.576360 ===
    application: emojifier
    exited: stopped
    type: temporary
%%% emojifier_SUITE ==> emojify: FAILED
%%% emojifier_SUITE ==>
{undef,[{emojifier,emojify,[[]],[]},
        {emojifier_SUITE,emojify,1,
                         [{file,"test/emojifier_SUITE.erl"},
                          {line,18}]},
        {test_server,ts_tc,3,[{file,"test_server.erl"},{line,1783}]},
        {test_server,run_test_case_eval1,6,
                     [{file,"test_server.erl"},{line,1292}]},
        {test_server,run_test_case_eval,9,
                     [{file,"test_server.erl"},{line,1224}]}]}
EXPERIMENTAL: …
Failed 1 tests. Passed 2 tests.
Results written to "_build/test/logs/index.html".
===> Failures occurred running tests: 1
# PRESENTING CODE

The Supervisor Module

-module emojifier_sup.

-behaviour supervisor.

-export [start_link/0, init/1].

start_link() ->
    supervisor:start_link(
        {local, emojifier_sup}, emojifier_sup, noargs).
    
init(noargs) ->
    {ok, {#{}, []}}.
src/emojifier_sup.erl
-module emojifier_sup.

-behaviour supervisor.

-export [start_link/0, init/1].

start_link() ->
    supervisor:start_link(
        {local, emojifier_sup}, emojifier_sup, noargs).
    
init(noargs) ->
    {ok, {#{}, [
        #{id => worker, start => {emojifier_wrk, start_link, []}}
    ]}}.
src/emojifier_sup.erl
# PRESENTING CODE

The Worker Module

-module emojifier_wrk.
-export [start_link/0, emojify/2, add/3].
-export [init/1, handle_call/3, handle_cast/2].
-behaviour gen_server.

start_link() -> gen_server:start(emojifier, noargs, []).
init(noargs) -> {ok, #{}}.
emojify(Emojifier, String) ->
    gen_server:call(Emojifier, {emojify, String}).
handle_call({emojify, String}, _From, Emojis) ->
    {reply,
     lists:flatten(maps:fold(
        fun(Text, Emoji, AccString) ->
            string:replace(AccString, Text, Emoji, all)
        end, String, Emojis)),
     Emojis}.
add(Emojifier, Text, Emoji) ->
    gen_server:cast(Emojifier, {add, Text, Emoji}), Emojifier.
handle_cast({add, Text, Emoji}, Emojis) ->
    {noreply, Emojis#{Text => Emoji}}.
src/emojifier_wrk.erl
-module emojifier_wrk.
-export [start_link/0, emojify/1, add/2].
-export [init/1, handle_call/3, handle_cast/2].
-behaviour gen_server.

start_link() ->
    gen_server:start_link(
    	{local, emojifier_wrk}, emojifier_wrk, noargs, []).

init(noargs) -> {ok, #{}}.

emojify(String) ->
    gen_server:call(emojifier_wrk, {emojify, String}).

handle_call({emojify, String}, _From, Emojis) -> …

add(Text, Emoji) ->
    gen_server:cast(emojifier_wrk, {add, Text, Emoji}).

handle_cast({add, Text, Emoji}, Emojis) -> …
src/emojifier_wrk.erl
# PRESENTING CODE

The Application Module

-module emojifier.

-behaviour application.

-export [start/2, stop/1].

start(_, noargs) ->
    emojifier_sup:start_link().

stop(nostate) -> ok.
src/emojifier.erl
-module emojifier.

-behaviour application.

-export [start/2, stop/1].
-export [emojify/1, add/2].

start(_, noargs) ->
    emojifier_sup:start_link().

stop(nostate) -> ok.

emojify(String) -> emojifier_wrk:emojify(String).

add(Text, Emoji) -> emojifier_wrk:add(Text, Emoji).
src/emojifier.erl
# PRESENTING CODE

Run the Tests

$ rebar3 ct
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling emojifier
===> Running Common Test suites...
%%% emojifier_SUITE: ...
All 3 tests passed.
$ 
# PRESENTING CODE

Check the Test Coverage

$ rebar3 ct --cover
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling emojifier
===> Running Common Test suites...
%%% emojifier_SUITE: ...
All 3 tests passed.
$ 
$ rebar3 ct --cover
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling emojifier
===> Running Common Test suites...
%%% emojifier_SUITE: ...
All 3 tests passed.
$ rebar3 cover --verbose
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling emojifier
===> Performing cover analysis...
  |------------------------|------------|
  |                module  |  coverage  |
  |------------------------|------------|
  |             emojifier  |      100%  |
  |         emojifier_sup  |      100%  |
  |         emojifier_wrk  |      100%  |
  |------------------------|------------|
  |                 total  |      100%  |
  |------------------------|------------|
  coverage calculated from: _build/test/cover/ct.coverdata
  cover summary written to: _build/test/cover/index.html

Stage ∞:

Learning more!

Using TDD with PropER and more…

Talk to these people

Pablo Costas Sanchez

Fred Hebert

This Weird Dude

Everything you always wanted to know about testing on the BEAM

By Brujo Benavides

Everything you always wanted to know about testing on the BEAM

Everything you always wanted to know about testing on the BEAM (by Mr. Wizard & Mr. Shores @ CodeBEAM America 2024)

  • 41