TDD💕Erlang

by @elbrujohalcon 🧙‍♂️

at CodeBEAM Europe 2023

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

TDD 💕 Erlang

By Brujo Benavides

TDD 💕 Erlang

TDD 💕 Erlang @ CodeBEAM Europe 2023

  • 178