Brujo Benavides
Erlang Developer and Trainer. Working at Baxter. Member of the Education WG @ The EEF. …long distant walker, amateur table tennis player and proud father!
A software development practice that emphasizes writing tests before writing the actual code
Using TDD to Learn Erlang
# PRESENTING CODE
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
-module emojifier.
emojifier.erl
# PRESENTING CODE
-module emojifier.
-export [test/0].
test() ->
…
'✅'.
emojifier.erl
# PRESENTING CODE
-module emojifier.
-export [test/0].
test() ->
{ok, _} = emojifier:start(),
'✅'.
emojifier.erl
# PRESENTING CODE
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
-module emojifier.
-export [test/0].
-export [start/0].
test() ->
{ok, _} = emojifier:start(),
'✅'.
start() -> {ok, #{}}.
emojifier.erl
# PRESENTING CODE
1> {ok, emojifier} = c(emojifier), emojifier:test().
'✅'
2>
# PRESENTING CODE
-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
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
-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
1> {ok, emojifier} = c(emojifier), emojifier:test().
'✅'
2>
# PRESENTING CODE
-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
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
-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
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
-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
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
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
1> {ok, emojifier} = c(emojifier), emojifier:test().
'✅'
2>
# PRESENTING CODE
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
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
1> {ok, emojifier} = c(emojifier), emojifier:test().
'✅'
2>
Using TDD with OTP Behaviours
# PRESENTING CODE
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
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
1> {ok, emojifier} = c(emojifier), emojifier:test().
'✅'
2>
# PRESENTING CODE
1> {ok, emojifier} = c(emojifier, [debug]), emojifier:test().
emojifier.erl:7:2:
Warning: undefined callback function handle_info/2
(behaviour 'gen_server')
'✅'
2>
Test the Callback
Pros:
Cons: Test ≄ Design
# Tradeoff
Leave the Warning
Pros:
Cons: Warnings.
Implement Callback
Pros: No Warnings
Cons:
# PRESENTING CODE
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
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
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
1> {ok, emojifier} = c(emojifier), emojifier:test().
'✅'
2>
Using TDD with rebar3 and CT
# PRESENTING CODE
{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
{application,
emojifier,
[{description, "Add magic to your strings 🧙♂️"},
{vsn, git},
{id, "emojifier"},
{applications, [kernel, stdlib]},
{mod, {emojifier, noargs}}]}.
src/emojifier.app.src
# PRESENTING CODE
$ rebar3 ct
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling emojifier
===> Running Common Test suites...
All 0 tests passed.
$
# PRESENTING CODE
-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
$ 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
-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
-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
$ 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
-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
$ 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
-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
$ rebar3 ct
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling emojifier
===> Running Common Test suites...
%%% emojifier_SUITE: ..
All 2 tests passed.
$
# PRESENTING CODE
-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
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
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
$ 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
-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
-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
-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
$ rebar3 ct
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling emojifier
===> Running Common Test suites...
%%% emojifier_SUITE: ...
All 3 tests passed.
$
# PRESENTING CODE
$ 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
Using TDD with PropER and more…
By Brujo Benavides
Everything you always wanted to know about testing on the BEAM (by Mr. Wizard & Mr. Shores @ CodeBEAM America 2024)
Erlang Developer and Trainer. Working at Baxter. Member of the Education WG @ The EEF. …long distant walker, amateur table tennis player and proud father!