test_utils

Build StatusHex.pmCoverage Status

Description

Erlang application that can be imported in your project to help in development of unit and property-based tests (using PropEr).

To import test_utils you need to add it to your rebar.config file e.g.

{profiles, [
    {test, [
        {deps, [test_utils]}
    ]}
]}.

Next step is to include testing.hrl in your testing modules e.g.

-module(my_module_tests).

-include_lib("test_utils/include/testing.hrl").
...

Unit tests helpers

?LOOPER(Fun, Args, LoopTimeout, Count)

This macro is used to call function Fun with arity matching number of arguments in the Args list for Count times until all assertions in the function pass. There's a timeout of LoopTimeout milliseconds between consecutive calls of the function. If function fails for Count times then output from last call is returned to the test.

ExpectedState = #state{id = ?KEY, waiting = []},
?LOOPER(fun(Key) ->
        {ok, State} = server:get_state(Key),
        ?assertEqual(ExpectedState, State)
    end,
    [?KEY],
    50, 10
).

?WAIT_FOR_PROCESS_STOPPED(NameOrPID)

Wait 1 second until process specified by its registered name or PID is stopped. If registered name is used this macro waits until process name is unregistered.

?WAIT_FOR_PROCESS_STOPPED(test_server).

In case process isn't stopped the following error is returned in the test:

  1) server_tests:process_stopped_test/0
     Failure/Error: unknown assert: "Process <0.341.0> wasn't stopped!"

?assertContains(Elem, List)

Assert that element Elem exists in list of elements List.

?assertContainsAll(Elems, List)

Assert that all of elements in list Elems exist in list of elements List.

?assertNotContains(Elem, List)

Assert that element Elem doesn't exist in list of elements List.

?assertJsonEquals(Expected, Actual)

Assert that JSON objects are equal. Both of them can be either encoded as a string or decoded to format compatible with jsx.

Function mock helpers

Macros described in this section can be used to mock behavior of tested functions using meck library and assert if tested code was called with expected arguments.

?MECK(Module, Funs)

Mock module Module with a list of functions in the following format:

Funs :: [
    {FunctionName :: atom(), Fun :: fun()} |
    {FunctionName :: atom(), FunResult :: any()}
]

If mocked function exists and Fun with specific arity is used for mocking the result then only function with such arity is mocked.

If function result is the Erlang term FunResult then all functions with matching name (no matter what arity) returns this Erlang term. If function doesn't exist then function with arity 0 is created and returns function result.

-define(TS, 1563443613000).

?MECK(ts_utils, [
    {get_os_timestamp, ?TS},
    {get_os_timestamp, fun
        (secs) -> ?TS div 1000;
        (millis) ->?TS
    end},
    {convert_timestamp, fun(T) -> T * 1000 end}
]).

---

1> ts_utils:get_os_timestamp().
1563443613000
2> ts_utils:get_os_timestamp(millis).
1563443613000
3> ts_utils:get_os_timestamp(secs).
1563443613
4> ts_utils:convert_timestamp(1563443781).
1563443781000

?MECK_LOOP(Module, Funs)

Mock module Module with a list of functions in the following format:

Funs :: [
    {FunctionName :: atom(), FunResults :: [any(), ...]}
]

Each mocked function result is specified as a list of values to be returned. Values in the list are returned sequentially in consecutive calls to mocked function. If end of the list is reached then values are returned from the beginning of the list again.

-define(TS, 1563443613000).

?MECK(ts_utils, [
    {get_os_timestamp, [?TS, ?TS + 1000, ?TS + 2000]}
]).

---

1> ts_utils:get_os_timestamp().
1563443613000
2> ts_utils:get_os_timestamp().
1563443614000
3> ts_utils:get_os_timestamp().
1563443615000
4> ts_utils:get_os_timestamp().
1563443613000

?assertCalledOnce(Module, Function, Args)

Assert if mocked function was called with specific arguments exactly once. Matchers like '_' can be used for matching of the arguments.

?assertCalledOnce(ts_utils, get_os_timestamp, [millis])

If mocked function was not called or was called more than 1 time then the assertion error is returned and outputs history of calls to mocked function.

Failure/Error: ?assertThat("ts_utils:get_os_timestamp(millis) called 1 time(s)")
  expected: {ts_utils,get_os_timestamp,[millis]}
       got: [{ts_utils,get_os_timestamp,[secs]},
             {ts_utils,get_os_timestamp,[millis]},
             {ts_utils,get_os_timestamp,[millis]}]

?assertNotCalled(Module, Function)

Assert if mocked function was not called at all.

?assertNotCalled(ts_utils, get_os_timestamp)

Assertion error is returned if there was a call made to function of any arity.

Failure/Error: ?assertThat("ts_utils:get_os_timestamp(...) called 0 time(s)")
  expected: not_called
       got: [{ts_utils,get_os_timestamp,[]},
             {ts_utils,get_os_timestamp,[secs]},
             {ts_utils,get_os_timestamp,[millis]}]

?assertNotCalled(Module, Function, Args)

Assert if mocked function was not called at all with given arguments. Matchers like '_' can be used for matching of the arguments.

?assertNotCalled(ts_utils, get_os_timestamp, ['_'])

?assertCalled(NumCalls, Module, Function, Args)

Assert if mocked function was called with specific arguments exactly some given number of times. Matchers like '_' can be used for matching of the arguments.

?assertCalled(3, ts_utils, get_os_timestamp, [millis])

?MECK_RESET(Module)

Reset all counters for calls to mocked functions. Counters are used by macros like ?assertCalled and ?assertNotCalled for checking function calls.

?UNMECK(Module)

Reset mocks to given modules. If mocks were overwriting existing functions then the default function results are returned back again.

Property based tests generators

There's a set of helpful generators defined in prop_testing.hrl that can be used in your property based tests.

?EQC_STRING_GEN

Generate random string with printable ASCII characters (ASCII codes 32 - 126)

"s"
"_"
"n"
"P"
...
"#1[<v8+.O9MQw"
"N<E?s=658Vt>@$&zV@\"-jY@#X7xtS-8NRR>@lvZWl"
"w1Sf <LQ\\2y_**U.:\")SYhuj4d"
"`\"al/s!muWWZ{L0`mBaX_y[C(j{~"

?EQC_ATOM_GEN

Generate random atom with printable ASCII characters (ASCII codes 32 - 126)

'}'
'\\'
w
'4'
...
'C&XAUV[a2pZ"y{`7?'
'FBnuB H3Xr`(d3Jq/FQl3{dXyIxS]515n'
'@<$@Fu*kyPaI].gXL1P9}vmUTDL"hw'
')03jvN"OD8OEaWnoV!/v`5\'jZ/g~6i`AKt/a'

?EQC_BYTE_GEN

Generate random unsigned byte value (values 0..255)

141
99
169
180
...
242
16
158
150

?EQC_USHORT_GEN

Generate random unsigned short value (values 0..65535)

17211
20042
23461
40286
...
8457
37070
28076
31370

?EQC_ULONG_GEN

Generate random unsigned long value (values 0..18446744073709551615)

0
0
3
2
...
75
68
25
13

?EQC_BIN_GEN(MaxSize)

Generate random binary of size 0..MaxSize

<<94,30,5,155>>
<<58,23>>
<<67,128,216,104,20,73,91>>
<<77,189,139,35>>
...
<<94,200,83,119,36,130>>
<<230,124,131,78,0,145,114,34,222,196>>
<<254,116,230,135,197,27>>

?EQC_UNIQUE_LIST_GEN(Gen)

Generate list of values generated by Gen generator that contain unique values

[]
[1]
[1,0]
[5]
[4,1]
...
[4,5,1,3,0,2]
[5,2,4,1]
[4,0,3,1,5,2]
[2,0,5,1,4,3]

?EQC_SORTED_UNIQUE_LIST_GEN(Gen)

Generate list of values generated by Gen generator that contain unique and sorted values

[5]
[]
[2,5]
[4,5]
[0,3]
...
[1,2,3,4,5]
[0,1,2,3,5]
[0,1,2,3,4,5]
[0,1,2,3,4,5]

Unit test progress logging

When you've added test_utils as a dependency to your project you can override default eunit_progress.erl reporting module that is used by rebar3 eunit. To do this you need to add the following configuration to your rebar.config file:

{eunit_opts, [
    {report, {test_progress_logger, #{
        handler_config => #{config => #{file => "test.eunit"}}
    }}}
]}.

After running unit tests you'll end up with test.eunit file that contains application and test logs.

The following options for test_progress_logger are supported: