Escalus
Escalus is an Erlang XMPP client library. It began as a tool for convenient testing of XMPP servers, but can also be used as a standalone Erlang application.
Escalus is aimed at checking correctness of XMPP server behaviour, in contrast to tools such as Tsung which are about stress testing and don't verify correctness.
This tool, escalus, is used by ESL's amoc for load tests against ESL's MongooseIM.
Quick start
The test/example_SUITE.erl file contains a minimalistic example of an Escalus test suite.
You should include escalus.hrl
file and the Common Test header:
-include_lib("escalus/include/escalus.hrl").
-include_lib("common_test/include/ct.hrl").
Escalus contains functions escalus:init_per_suite/1
,
escalus:end_per_suite/1
, escalus:init_per_testcase
and
escalus:end_per_testcase
which should be called in
appropriate Common Test callback functions.
Calling escalus:init_per_testcase
is mandatory as this function
initializes the runtime support for escalus:story
(i.e. escalus_cleaner
-- actually, you can do it manually if you know
what you're doing).
You can specify users that will take part in your tests in Common Test config files, look at test/test.config file that comes with Escalus:
{escalus_users, [
{alice, [
{username, "alice"},
{server, "localhost"},
{password, "makota"}]},
{bob, [
{username, "bob"},
{server, "localhost"},
{password, "bobcat"}]}
]}.
Escalus can create and delete those users in two ways:
- using in-band registration XEP-0077
when it is supported by the server and has no limits on number of registrations
per second (configure
registration_timeout
toinfinity
in case of ejabberd). - using erlang rpc calls to the ejabberd_admin:register/2 function (in case of MongooseIM or ejabberd as the tested server and the in-band registration is disabled)
You create and delete the users by calling escalus:create_users/1
and escalus:delete_users/1
:
init_per_group(_GroupName, Config) ->
escalus:create_users(Config).
end_per_group(_GroupName, Config) ->
escalus:delete_users(Config).
In our exemplary test case it is done in init_per_group
and end_per_group
functions, but you could as well do it in init-/end_per_suite
if you prefer.
Deleting users should clean all their data (e.g. roster buddies), so it
improves test isolation, but takes longer.
In most of the test cases you will use escalus:story/3
function.
Story wraps all the test and does the cleanup and initialisation:
messages_story(Config) ->
escalus:story(Config, [1, 1], fun(Alice, Bob) ->
%% Alice sends a message to Bob
escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"OH, HAI!">>)),
%% Bob gets the message
escalus:assert(is_chat_message, [<<"OH, HAI!">>],
escalus:wait_for_stanza(Bob))
end).
The story above involves two users (second argument is a two-element list) each having one resource (list contains ones). As you see from the config files, those users are Alice and Bob. Escalus logs in users at the beginning of the story and logs them out after it ends (either successfully or by crash).
It's also possible to designate users taking part in a story more specifically:
messages_story(Config) ->
escalus:story(Config, [{alice, 1}, {kate, 1}], fun(Alice, Kate) ->
...
end).
That allows one to choose users which are not consecutive in test/test.config.
Inside the story you can use escalus:send/2
function to send
stanzas, functions from escalus_stanza
module to create them
and escalus:wait_for_stanza
to receive them.
wait_for_stanza
makes test fail if no stanza arrives
up to a timeout. There is also wait_for_stanzas
function which
takes number of stanzas N as an argument and returns N-element or
shorter list of stanzas, returning less stanzas instead of crashing.
Both wait_for_stanza
and wait_for_stanzas
can take an extra argument --
timeout in milliseconds. The default timeout value is one second.
You make assertions using escalus:assert/3
function.
First argument is the predicate. It can be a fun,
a {module, function}
tuple or an atom. Atoms refer
to functions from escalus_pred
module. Second
argument is a parameter list and third is a stanza that
we assert things about. There is escalus:assert/2
function that is equivalent to assert/3
with empty
parameter list. Calling escalus:assert(Pred, [Param1, Param2], Stanza)
makes sure that Pred(Param1, Param2, Stanza) yields true.
Stanza is separate from parameters to improve error reporting.
Escalus as a standalone application
It's possible to use Escalus as a standalone application,
i.e. outside a Common Test test suite (and without any reliance
on the common_test
application and its modules).
If you use rebar3 tool there are only few steps to generate
full escalus release. Just type in your bash shell:
rebar3 release
and wait until it finishes. It is now possible to start erlang shell with command:
$ESCALUS_ROOT/_build/default/rel/escalus/bin/escalus
You can now enjoy usage of escalus application in your erlang release! In order to use escalus as standalone application without rebar3 some prerequisites must be met.
Firstly, Escalus must be started just like any other application:
> application:ensure_all_started(escalus).
This makes predefined environment variables from escalus.app
available
for access by application:get_env
.
These options and their respective values for running without Common Test are:
{env, [{config_file, "priv/escalus.config"}]}
To recap:
config_file
must be set to a configuration file location; this location may be absolute or relative (in which case the file will be searched for relative to the project directory).
Note, that in a real security-conscious setting you probably shouldn't store clear text user passwords in this file (though that's exactly what the example does - remember Escalus is still mostly a testing tool).
If you don't want to rely on the application resource file
(escalus.app
/escalus.app.src
) you can set both of these options just
after loading Escalus:
> application:ensure_all_started(escalus).
> application:set_env(escalus, config_file, "/absolute/or/relative/path").
Keep in mind that calling application:ensure_all_started(escalus)
will
overwrite the values with stuff from escalus.app
.
Set the variables after the application is started.
Config file location
If the config_file
value starts with /
it's interpreted as an
absolute path and left as is.
Otherwise, it's interpreted as a relative path to the project directory.
The project directory is the directory one level higher than the directory
containing ejabberd_ct.beam
.
In case of a standard Git checkout the project directory is simply escalus
.
escalus/
โโโ .git/
โโโ ...
โโโ docs/
โโโ ebin/
โย ย โโโ ...
โย ย โโโ escalus_ct.beam
โย ย โโโ ...
โโโ src/
โโโ ...
Example shell session
Fire an Erlang shell:
erl -pa ebin deps/*/ebin
Basic example
Run example:
application:ensure_all_started(escalus).
{ok, Config} = file:consult("priv/escalus.config").
CarolSpec = escalus_users:get_options(Config, carol).
{ok, Carol, _, _} = escalus_connection:start(CarolSpec).
escalus_connection:send(Carol, escalus_stanza:chat_to(alice, "hi")).
escalus_connection:stop(Carol).
Story example
Please note that escalus:story/3
and escalus:create_users/2
are intended to be used in a testing environment, i.e. with Common Test available. Specifically, escalus:create_users/2
will not work without Common Test and with non-XMPP registration method chosen (i.e. RPC based user registration). In case of MongooseIM or ejabberd, please ensure mod_register
is enabled (and, depending on your scenario, probably configured not to send a welcome message).
Run example:
X2SFun = fun(X) -> lists:flatten(io_lib:format("~p~n", [X])) end.
{ok, Config0} = file:consult("priv/escalus.config").
application:ensure_all_started(escalus).
escalus:create_users(Config0, {by_name, [alice, mike]}).
Config = escalus_event:start(escalus_cleaner:start(Config0)).
SendFun = fun(A, B) -> escalus:send(A, escalus_stanza:chat_to(B, "hi")), ok end.
RecvFun = fun(B) -> [S] = escalus:wait_for_stanzas(B, 1), {ok, S} end.
StoryFun = fun(A, B) -> SendFun(A, B), {ok, S} = RecvFun(B), erlang:display(X2SFun(S)) end.
escalus:story(Config, [{mike, 1}, {alice,1}], StoryFun).
escalus_cleaner:stop(escalus_event:stop(Config)).
escalus:delete_users(Config, {by_name, [alice, mike]}).
Naming
According to Wikipedia, Prince Escalus, of the House Escalus, is the voice of authority in Verona, and appears only three times within the text and only to administer justice.
It follows the great tradition to use characters of William Shakespeare's Romeo and Juliet in the XMPP specifications.