Пример использования Common Test, EUnit и Meck
15 мая 2013
В этой заметке рассматривается написание автоматических тестов на Erlang. Автотесты помогают находить не только мелкие ошибки, случайно допущенные при внесении изменений в коде, но и серьезные, сложные в обнаружении, ошибки, такие, как состояние гонки. Также тесты полезны по той причине, что чем больше ситуаций и способов использования модуля в них проверяется, тем более продуманные интерфейсы мы пишем.
Давайте покроем тестами библиотеку erlymemo. Попробуем придумать десяток тестов:
- Функция, вызываемая через erlymemo, должна возвращать свой результат;
- После второго вызова функции через erlymemo размер ETS не должен меняться;
- При двух последовательных вызовах функции через erlymemo должен произойти только один вызов, первый;
- При вызове большого количества разных функций с разными аргументами размер ETS не должен превышать предела, заданного в конфиге;
- Вызываем функцию A, делаем много вызовов B, снова вызываем A, в результате функция A должна вызваться дважды;
- После вызова
erlymemo:clean/0
ETS должная стать пустой; - Запускаем 100 процессов, каждый из которых делает по несколько тысяч случайных вызовов одной из трех функций, в результате функции должны работать правильно, ничто не должно упасть;
- При вызове несуществующей функции бросается исключение;
- При вызове функции с неверными аргументами бросается исключение;
- Если приложение остановлено, при вызове функции, запросе текущего размера таблицы и тд бросается исключение;
Для написания автоматических тестов огромной популярностью в мире Erlang пользуется фреймворк Common Test. Описать все его возможности в рамках одного поста не представляется возможным, к тому же, я все равно их все не знаю. Мы рассмотрим лишь основы использования Common Test.
Тесты принято складывать в каталоге test исходного кода проекта. Тесты объединяются в наборы тестов, которые хранятся в файлах с именами имя_набора_SUITE.erl
. Типичная структура набора тестов выглядит так:
-compile(export_all).
-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
all() ->
[
% список тестов
].
init_per_suite(Config) ->
% действия, выполняемые перед запуском набора тестов
Config.
init_per_testcase(_, Config) ->
% действия, выполняемые перед запуском теста
Config.
end_per_testcase(_, Config) ->
% действия, выполняемые после завершения теста
Config.
end_per_suite(Config) ->
% действия, выполняемые после завершения всего набора тестов
Config.
% код тестов
% обратите внимание, что ни одно из объявлений
% init_*/end_* функций не является обязательным
В нашем случае перед запуском и после выполнения набора тестов мы будем просто запускать и останавливать тестируемое приложение соответственно:
ok = application:start(erlymemo),
Config.
end_per_suite(Config) ->
ok = application:stop(erlymemo),
Config.
С действиями, выполняемыми перед и после каждого теста, чуточку интереснее:
ets:new(?MODULE, [set, named_table, public]),
zero_call_counter(),
meck:new(foo),
meck:expect(foo, bar, fun(X, Y) -> inc_call_counter(), X + Y end),
meck:expect(foo, baz, fun(X, Y) -> X - Y end),
meck:expect(foo, qux, fun(X, Y) -> X * Y end),
erlymemo:clean(),
Config.
end_per_testcase(_, Config) ->
meck:unload(foo),
ets:delete(?MODULE),
Config.
Библиотека meck предназначена для создания mock-модулей. В функции init_per_testcase/2
мы создаем модуль foo с простыми функциями bar/2
, baz/2
и qux/2
, а в функции end_per_testcase/2
— удаляем его. Также создается/удаляется ETS-таблица, которую мы будем использовать для подсчета количества вызовов функции foo:bar/2
. Наконец, перед запуском каждого теста мы удаляем все данные, сохраненные в erlymemo.
Вам, вероятно, интересно, почему нельзя создавать ETS’ку и модуль foo в init_per_suite/1
. К сожалению, это не будет работать. ETS уничтожается после остановки создавшего его процесса (если только таблица не была передана другому процессу и не был указан процесс-наследник). Гарантируется, что init_per_testcase/2
и end_per_testcase/2
вызываются одним и тем же процессом. Для других функций таких гарантий нет.
Наконец, перейдем к нашему первому тесту. Функция, вызываемая через erlymemo, должна возвращать свой результат:
List = [1,2,3],
Result = erlymemo:call(erlang, length, [ List ]),
?assertEqual(3, Result).
Здесь мы использовали макрос ?assertEqual
, объявленный в файле eunit.hrl. Как несложно догадаться, он проверяет равенство своих аргументов. Вообще-то говоря, EUnit представляет собой совершенно самостоятельный фреймворк для модульного тестирования. Однако я нахожу его менее удобным и мощным, чем Common Test. Поэтому в рамках данной заметки из EUnit нам понадобятся только макросы. Использование макросов EUnit в Common Test, если что, является совершенно обыденным делом в мире Erlang’а.
Следующие несколько тестов довольно однообразны. Давайте не будем на них смотреть, а лучше сразу перейдем к интересному тесту. Запускаем 100 процессов, каждый из которых делает по несколько тысяч случайных вызовов одной из трех функций, в результате функции должны работать правильно, ничто не должно упасть:
Parent = self(),
PidList = [ spawn_link(
fun() ->
[ erlymemo:call(foo, Fun, [ Alpha, Beta ])
|| Fun <- [ bar, baz, qux ],
Alpha <- [ random:uniform(10) || _J <- lists:seq(1, 50) ],
Beta <- [ random:uniform(10) || _K <- lists:seq(1, 50) ]
],
Parent ! { ok, self() }
end)
|| _I <- lists:seq(1, 100) ],
lists:foreach(
fun(Pid) ->
receive
{ ok, Pid } -> ok
after
30000 -> throw('timeout')
end
end,
PidList).
Этот тест довольно прост, однако благодаря ему мне удалось отловить состояние гонки и соответствующим образом исправить библиотеку.
Рассмотрим еще один, последний тест. При вызове функции с неверными аргументами должно бросаться исключение:
?assertError(badarith, erlymemo:call(lists, sum, [ [1,2,3,bang] ])).
Помимо макросов, проверяющих равенство и тому подобные вещи, в EUnit есть очень полезные макросы ?assertError
, ?assertThrow
и ?assertExit
. Примеры использования каждого из них вы найдете в коде тестов к erlymemo на GitHub.
Если вы никогда раньше не работали с Common Test, обязательно попробуйте скачать исходники erlymemo, собрать их и прогнать тесты. Все это делается простой командой make
, если, конечно, у вас уже установлены Erlang и Rebar. Обратите внимание на созданный с помощью Common Test красивый HTML-отчет (файл logs/index.html), а также на то, что в этом отчете вы можете оценить степень покрытия кода тестами.
Дополнительные материалы:
- Весьма объемная документация по Common Test;
- Обсуждение проблемы использования ETS в CT на StackOverflow;
- Главы о Common Test и EUnit в «Learn You Some Erlang for Great Good!»;
- Meck на GitHub, не поленитесь заглянуть в исходники;
В сумме мною было написано тринадцать тестов к erlymemo, которые покрывают 91% кода. Не покрыты handle_call, handle_info и тп, потому что они все равно не используются.
Метки: Erlang, Разработка, Тестирование, Функциональное программирование.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.