Teal
Stratus3D
Description
An Erlang assertion library. Writing unit tests for Erlang applications is usually pretty straightforward. After all, most of the time you are just testing functions. Testing functions is easy because you just invoke a function with some parameters and then check if the expected value was returned. With pattern matching this is trivial. However, not everything is so simple. Say for example, you want to test that a module implements a certain behavior. You could do something like this:
% Check if a module exports all the callbacks defined in a behavior
Exports = module_under_test:module_info(exports),
lists:foreach(fun(Callback) ->
% Assert the callback is in the list of exported functions
true = lists:member(Callback, Exports)
end, [ % Callbacks defined in behaviour
{callback_1, 1},
{another_callback, 2},
{a_third, 0}
]).Or with Teal, it would be as simple as:
teal_behaviours:assert_implements_behaviour(module_under_test, expected_behavior).
Teal does all the hard work behind the scenes verifying that the module does in fact implement the behavior. And unlike the custom code in the first example, Teal's implements_behaviour function will work with any behavior.
I created Teal because I kept writing similar test helper functions in my unit tests. I tried to extract all the common patterns I saw in my test helpers into generic functions in Teal. This library by no means complete. I am sure there are common assertions that I missed. If you have anything that you think should be part of Teal feel free to create a pull request or issue on GitHub.
For Elixir, checkout Lilac, an Elixir wrapper for Teal.
Installation
To build Teal cd into the root and run make.
To make the Teal modules available during your tests. Added it to your ERL_LIBS environment variable(most likely defined in ~/.bashrc if you are using Bash):
export ERL_LIBS=/full/path/to/teal/
Or add it to your Erlang resource file (~/.erlang) like so:
code:load_abs("/full/path/to/teal/").All the Teal modules should now be able in your tests.
Usage
Once you have the Teal installed you should be able to use any of the functions below in your Common Test or EUnit test suites. You could also use Teal directly in your application if you wanted. But most of the functions are only useful in unit tests. For example, using functions like teal:raises_exception in your application code would violate Erlang's "fail fast" principle in most cases.
API
The API is documented below. Most of the functions listed return a boolean. Of the functions that return booleans, there are two additional variations of each function that are not documented below. By prefixing one of these functions with assert_ (e.g. teal_lists:includes_members becomes teal_lists:assert_includes_members) an exception is raised if the assertion fails instead of returning false. This allows you to ignore the return value since failure causes an error to be raised. Functions prefixed with assert_ can also take an additional argument. The extra argument is the message in the error that is raised when the assertion fails. This allows you to generate more readable failure messages.
teal
not_equal/2- Args:Term1 :: term(), Term2 :: term()Verifies that first term does not match the second.
teal:not_equal(a, b). %=> true teal:not_equal(a, a). %=> falseteal:raises_exception/1Args:Fun :: fun()Checks if an exception was raised when invoking
Fun. You are probably better of being more specific. Check the other functions below.teal:raises_exception(fun() -> error(err) end). %=> true teal:raises_exception(fun() -> true end). %=> falseteal:raises_exception_with_message/2Args:Fun :: fun(), ErrMsg :: term()Checks if an exception with a message matching
ErrMsgwas raised when invokingFun.ErrMsg = some_err_msg, teal:raises_exception_with_message(fun() -> error(ErrMsg) end, ErrMsg). %=> true teal:raises_exception_with_message(fun() -> error(another_err) end, ErrMsg). %=> false teal:raises_exception_with_message(fun() -> true end, ErrMsg). %=> falseteal:raises_throw/1Args:Fun :: fun()Checks if a throw exception was raised when invoking
Fun.teal:raises_throw(fun() -> throw(err) end). %=> true teal:raises_throw(fun() -> true end). %=> falseteal:raises_throw_with_message/2Args:Fun :: fun(), ErrMsg :: term()Checks if a throw exception with a message matching
ErrMsgwas raised when invokingFun.ErrMsg = some_err_msg, teal:raises_throw_with_message(fun() -> throw(ErrMsg) end, ErrMsg). %=> true teal:raises_throw_with_message(fun() -> throw(another_err) end, ErrMsg). %=> false teal:raises_throw_with_message(fun() -> true end, ErrMsg). %=> falseteal:raises_error/1Args:Fun :: fun()Checks if an error exception was raised when invoking
Fun.teal:raises_error(fun() -> error(err) end). %=> true teal:raises_error(fun() -> true end). %=> falseteal:raises_error_with_message/2Args:Fun :: fun(), ErrMsg :: term()Checks if an error exception with a message matching
ErrMsgwas raised when invokingFun.ErrMsg = some_err_msg, teal:raises_error_with_message(fun() -> error(ErrMsg) end, ErrMsg). %=> true teal:raises_error_with_message(fun() -> error(another_err) end, ErrMsg). %=> false teal:raises_error_with_message(fun() -> true end, ErrMsg). %=> falseteal:raises_exit/1Args:Fun :: fun()Checks if an exit was raised when invoking
Fun.teal:raises_exit(fun() -> exit(err) end). %=> true teal:raises_exit(fun() -> true end). %=> falseteal:raises_exit_with_message/2Args:Fun :: fun(), ErrMsg :: term()Checks if an exit with a message matching
ErrMsgwas raised when invokingFun.ErrMsg = some_err_msg, teal:raises_exit_with_message(fun() -> exit(ErrMsg) end, ErrMsg). %=> true teal:raises_exit_with_message(fun() -> exit(another_err) end, ErrMsg). %=> false teal:raises_exit_with_message(fun() -> true end, ErrMsg). %=> false
teal_lists
includes_members/2- Args:List :: list(), Members :: list()Checks if all members of the
Memberslist are present in theListlist. Examples:teal_lists:includes_members([1,2,3,4], [2,4]). %=> true teal_lists:includes_members([1,2,3,4], [5]). %=> falseteal_lists:same_members/2- Args:List1 :: list(), List2 :: list()Similar to
includes_members/2. Checks if all members inList1are present inList2and no extra members are present inList2. The only thing this does different than the=comparison is ignore the ordering of the items in the lists.teal_lists:same_members([1,2,3,4], [4,2,3,1]). %=> true teal_lists:same_members([1,2,3,4], [2,4]). %=> falseorder/2- Args:List :: list(), OrderFun :: list()Checks if all the members in
Listare already in the order specified by theOrderFun.Listis sorted byOrderFunand then matched againstList. If the two lists match the order is correct and the function returns true. Otherwise it returns false.Fun = fun(A,B) -> A =< B end, teal_lists:order([1,2,3,4], Fun). %=> true teal_lists:order([1,4,3,2], Fun). %=> false
teal_types
not_of_type/2- Args:Term :: any(), Type :: atom()Checks if
Termis not of typeTypeteal_types:not_of_type(a, atom). %=> false teal_types:not_of_type(<<"test">>, binary). %=> false teal_types:not_of_type(a, binary). %=> truenot_record/1- Args:Term :: any()Checks if
Termcould be a record. If the term is a tuple with an atom as the first valueTermis assumed to be a record and the function returns false. Otherwise it returns true.teal_types:not_record({foo, bar}). %=> false teal_types:not_record({[], a}). %=> true teal_types:not_record(not_a_record). %=> truecould_be_record/1- Args:Term :: any()Checks if
Termcould be a record. If the term is a tuple with an atom as the first value inTermtuple then the term could be a record and the function returns true. Otherwise it returns false. Opposite ofteal_types:not_record/1.teal_types:could_be_record({foo, bar}). %=> true teal_types:could_be_record({[], a}). %=> false teal_types:could_be_record(not_a_record). %=> false
teal_modules
is_module/1- Args:ModuleName :: atom()Checks if
ModuleNameis the name of an Erlang module.teal_modules:is_module(erlang). %=> true teal_modules:is_module(abc). %=> falseexports/2- Args:Module :: atom(), Function :: atom()Check if
Moduleexports a function with a name that matchesFunction.teal_modules:exports(erlang, port_call). %=> true teal_modules:exports(erlang, missing_fun). %=> falseexports_with_arity/3- Args:Module :: atom(), Function :: atom(), Arity :: integer()Check if
Moduleexports a function with a name that matchesFunctionand an arity ofArity.teal_modules:exports(erlang, port_call, 2). %=> true teal_modules:exports(erlang, port_call, 1). %=> false
teal_processes
is_registered/1- Args:Process :: pid() | atom()Checks
Processis registered.teal_processes:is_registered(RegisteredPidOrAtom). %=> true teal_processes:is_registered(UnregisteredPidOrAtom). %=> falseis_registered_with_name/2- Args:Process :: pid(), Name :: atom()Checks if
Processis registered withName. Similar toteal_processes:is_registered/1.RegisteredPidOrAtom = <0.43.0>, UnregisteredName = <0.112.0>, register(RegisteredName, RegisteredPidOrAtom), teal_processes:is_registered_with_name(RegisteredPidOrAtom, RegisteredName). %=> true teal_processes:is_registered_with_name(UnregisteredPidOrAtom, UnregisteredName). %=> falseget_state/1- Args:Process :: pid() | atom()Returns the state of the
Process.Processmust be either a gen_server, gen_event, or gen_fsm process.teal_processes:get_state(RegisteredPidOrAtom). %=> returns RegisteredPidOrAtom processes' state.receive_message/2- Args:Message :: term(), Timeout :: integer()Returns a pid, if the pid does not receive the given message before the timeout the pid raises an error.
Msg = test, function_that_sends_msg_to_self(Msg), teal_processes:receive_message(Msg, 500), %=> true teal_processes:receive_message(Msg, 500), %=> false
teal_behaviours
has_callback/3- Args:Module :: atom(), Name :: atom(), Arity :: integer()Checks if
Modulehas a function withNameandArity.teal_behaviours:has_callback(gen_server, handle_call, 3). %=> true teal_behaviours:has_callback(erlang, callback, 1). %=> falseis_behaviour/1- Args:Module :: atom()Checks if a
Moduleis also a behaviour. This is done by checking the module for callback attributes. If any are foundModuleis considered a behaviour.teal_behaviours:is_behaviour(gen_server). %=> true teal_behaviours:is_behaviour(erlang). %=> falseimplements_behaviour/2- Args:Module :: atom(), Behaviour :: atom()Check if
Moduleimplements all the callbacks defined inBehaviour. If functions with the same name and arity exist for each callback inBehaviourModuleis assumed to have implementedBehaviourcorrectly.teal_behaviours:implements_behaviour(supervisor, gen_server). %=> true teal_behaviours:implements_behaviour(erlang, gen_server). %=> false
teal_numbers
close_to/3- Args:Received :: float(), Value :: float(), Delta :: float()Checks if
Receivedis withinDeltaofValue. If it is the function returns true. Otherwise it returns false.teal_numbers:close_to(7, 10, 5). %=> true teal_numbers:close_to(4, 10, 5). %=> false
TODO
- Re-evaluate the 24 functions related to check for exceptions in teal.erl. Can the code be simplified? The code is not DRY at all right now.
-
Create a
teal_timemodule to handle time and date and datetime assertions. - Use Teal to test Teal? Not sure if this is a good idea or not.
-
Create the following assertions:
teal_os:command/1teal_os:command_status/2teal_os:command_output/2teal_otp:get_gen_server_state/1Args:Process :: pid() | atom()Returns the custom state of theProcess.
Similar Tools
Known Issues
No known issues. If you see something that could be improved feel free to open an issue on GitHub (https://github.com/Stratus3D/teal/issues)
Contributing
Feel free to create an issue or pull request if you find a bug or if you have additional assertions that you would like to be included in this library.