• Главная
  • Карта сайта
Не найдено

Unit тестирование в Erlang на примере

Опубликовано: 01.09.2018

видео Unit тестирование в Erlang на примере

19.07.2013, Приемочное тестирование на закате эпохи ПК

Продолжаем изучение языка программирования Erlang. На этот раз будет рассмотрена задача о разборе и выполнении арифметических выражений из книги Чезарини (в книге задача была сложнее и с множеством дополнений типа «упрощение выражений», «дополнение выражений оператором ветвления» и т.п.).


Использование сервис-ориентированной архитектуры (SOA) для построения сложных веб проектов

Помимо разбора выражений (строк) в статье описано Unit-тестирование последовательных программ без побочных эффектов с использованием стандартного модуля EUnit.

Сформулируем задачу:


Вызов Flussonic API с помощью .NET и Mono

Построить вычислитель выражений следующего вида:

<выражение> := <целое> | ~ <выражение> | (<выражение> <оператор> <выражение>); <целое> := <цифра> | <цифра> <целое>; <цифра> := 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; <оператор> := + | - | *;

Например,(7+~((2+3)-4)), тильда обозначает унарный минус.

Для решения задачи удобно преобразовать строку-выражение в формат, более пригодный для вычисления. Более удобной структурой может являться например дерево разбора (синтаксическое дерево) или обратная польская запись (ПОЛИЗ) [1].

Чезарини предлагает преобразовать выражение в дерево разбора, в котором:

константы представлены кортежем { num, Value }; бинарные операторы — { тип оператора, аргумент 1, аргумент 2 }; унарные операторы — { тип оператора, аргумент };

Позже Чезарини пишет про стековую машину (а она подразумевает использование ПОЛИЗ), но мы упростим задачу и проведем вычисления прямо на дереве разбора.

Итак, у нас в программе будет 3 части:

преобразователь арифметического выражения, заданного строкой в синтаксическое дерево; преобразователь синтаксического дерева в строку (удобно для проверки); вычислитель выражений, заданных деревом.

Разбор и вычисление арифметических выражений

Для разбора нам потребуется несколько вспомогательных функций, самая большая из них — get_num (считывает целое число из начала строки). Если в начале строки нет цифры — функция завершится неудачей.

% ---------------------------------- % get_num считывает целое беззнаковое число % использует get_strNum % ret: {строка-результат, остаток строки} get_num([H|T]) when H >= $0, H =< $9 -> { TN, TR } = get_strNum([H|T]), { list_to_integer(TN), TR }. % ---------------------------------- % get_strNum вспомогательная функция get_num, % считывает число в виде строки get_strNum([H|T]) when H >= $0, H =< $9 -> { TN, TR } = get_strNum(T), { [H|TN], TR }; get_strNum(L) -> {[], L}.

Кроме того, нам потребуется функция, кодирующая операторы как показано ниже:

% ---------------------------------- % operator преобразует символьное представление оператора в константу operator($-) -> minus; operator($+) -> plus; operator($*) -> mul.

Когда у нас появились такие функции, мы можем приступить непосредственно к разбору выражения:

% ---------------------------------- % exp_with_tail вспомогательная функция exp, возвращает конец строки в кортеже % выражение -> целое | ~выражение | (выражение оператор выражение) % ret: { результат, остаток строки } exp_with_tail([H|T]) when H >= $0, H =< $9 -> % целое { Num, TR } = get_num([H|T]), { {num, Num}, TR }; exp_with_tail([$~|T]) -> % выражение с унарными минусом {Exp, TR} = exp_with_tail(T), { {un_min, Exp}, TR }; exp_with_tail([$(|T]) -> % выражение в скобках (с оператором) {Op1, TR1} = exp_with_tail(T), % первый операнд [Op|TR2] = TR1, % оператор {Op2, TR3} = exp_with_tail(TR2), % второй операнд [$)|TR] = TR3, { {operator(Op), Op1, Op2}, TR }.

При помощи охраняющих выражений и сопоставления с шаблоном функция exp_with_tail определяет тип первого элемента строки, и производит рекурсивную обработку остатка строки. Такая обработка возможна за счет того, что exp_with_tail помимо дерева возвращает необработанную часть строки, однако, пользователю, вызывающему функцию эта информация ненужна — ее можно скрыть за счет функции list_to_exp следующего вида:

% ---------------------------------- % list_to_exp разбирает арифметические выражения вида: % выражение -> целое | ~выражение | (выражение оператор выражение) % использует вспомогательную функцию exp_with_tail % результат - выражение для стек-машины в префиксной записи % пример {minus, {plus, {num, 1}, {num, 2}}, {num, 3}} соответствует ((1+2)-3) list_to_exp(S) -> {R, _} = exp_with_tail(S), R.

Теперь у нас есть функция, преобразующая строку с арифметическим выражением в дерево синтаксического разбора, с которым удобно работать. При помощи одного обхода можно преобразовать это дерево в ПОЛИЗ — эту часть я предлагаю решить читателям блога и прикрепить в комментариях к статье.

Дальше мы посмотрим как производится юнит-тестирование в Erlang, и при этом нам очень удобно будет использовать функцию, производящую действия, обратные функции list_to_exp. Функция exp_to_list преобразует дерево разбора в выражение-строку.

% ---------------------------------- % exp_to_list - преобразователь выражения в строку exp_to_list({num, X}) -> integer_to_list(X); exp_to_list({un_min, X}) -> [$~|exp_to_list(X)]; exp_to_list({plus, A, B}) -> [$(|exp_to_list(A)] ++ [$+|exp_to_list(B)] ++ [$)]; exp_to_list({minus, A, B}) -> [$(|exp_to_list(A)] ++ [$-|exp_to_list(B)] ++ [$)]; exp_to_list({mul, A, B}) -> [$(|exp_to_list(A)] ++ [$*|exp_to_list(B)] ++ [$)].

Как видим, функция очень проста — при помощи сопоставления с образцом определяется тип узла дерева, производится его обработка и рекурсивная обработка дочерних элементов.

Функция exp_calc вычисления такого выражения выглядит еще проще (и работает по тому же принципу что и exp_to_list).

% ---------------------------------- % exp_calc - вычислитель выражения exp_calc({num, X}) -> X; exp_calc({un_min, X}) -> -exp_calc(X); exp_calc({plus, A, B}) -> exp_calc(A) + exp_calc(B); exp_calc({minus, A, B}) -> exp_calc(A) - exp_calc(B); exp_calc({mul, A, B}) -> exp_calc(A) * exp_calc(B).

Переходим к тестированию…

Unit-тестирование в Erlang

Когда мы пишем программы, мы постоянно запускаем только что написанные ее части чтобы проверить правильность работы. Например, я мог бы запускать на разных примерах exp_calc и сверять результаты с ожидаемыми.

Кроме того, в нормальную программу постоянно вносятся изменения и дополнения. Очень хорошо бы проверять правильность работы программы после каждого изменения, но не всегда это делают (ведь лень) — в конторе где я писал игрушки несколько раз в релизную ветку проекта коммитили непроверенный код, который даже не компилировался.

Юнит-тесты не решают проблему лени, но значительно упрощают тестирование. Например, нам потребовалось добавить в функцию exp_calc поддержку оператора деления, ответственный программист напишет код, запустит его пару раз и проверит работу новой фичи. Однако, изменяя уже написанный код, он мог нечаянно что-то сломать (например, иногда неправильно стало работать умножение).

Тестировать код руками после каждого, даже незначительного, изменения очень трудно, однако, этот процесс можно автоматизировать (при помощи юнит-тестов). При этом мы лишь один раз формируем наборы входных данных и эталонов результата, а затем запускаем тестирование после каждого внесенного изменения. Если что-то пойдет не так — мы узнаем номера тестов, которые не сработали.

Unit-тесты в Erlang помещаются внутрь функций, имена которых имеют постфикс «_test». Например, exp_calc_test. Внутри таких функций записываются утверждения, которые должны быть верны, например мы можем записать:

list_to_exp_test() -> {num,1} =:= list_to_exp('1'), ?assertEqual({minus,{plus,{num,1},{num,2}},{num,3}}, list_to_exp('((1+2)-3)')).

Приведенный пример содержит 2 теста. В третьей строке показано использование макроса ?assertEqual, который рекомендуется использовать вместо оператора сравнения в тестах, т.к. он дает больше информации в случае ошибки. Кроме макроса ?assertEqual существуют и другие [2], но все они определяются через макрос ?assert (который тоже можно использовать).

Ниже приведены примеры юнит-тестов для функций, описанных в предыдущем разделе:

list_to_exp_test() -> ?assertEqual({num,1}, list_to_exp('1')), ?assertEqual({un_min, {num,1}}, list_to_exp('~1')), ?assertEqual({minus,{plus,{num,1},{num,2}},{num,3}}, list_to_exp('((1+2)-3)')), ?assertEqual({un_min, {plus, {mul, {num, 2}, {num, 3}}, {mul, {num, 3}, {num, 4}} } }, list_to_exp('~((2*3)+(3*4))')). calc_test() -> ?assertEqual(1, exp_calc(list_to_exp('1'))), ?assertEqual(-1, exp_calc(list_to_exp('~1'))), ?assertEqual(0, exp_calc(list_to_exp('((1+2)-3)'))), ?assertEqual(-18, exp_calc(list_to_exp('~((2*3)+(3*4))'))). exp_to_list_test() -> ?assertEqual('1', exp_to_list(list_to_exp('1'))), ?assertEqual('~1', exp_to_list(list_to_exp('~1'))), ?assertEqual('((1+2)-3)', exp_to_list(list_to_exp('((1+2)-3)'))), ?assertEqual('~((2*3)+(3*4))', exp_to_list(list_to_exp('~((2*3)+(3*4))'))).

В модуль, содержащий описание тестов должен быть включен файл «eunit/include/eunit.hrl» при помощи директивы include_lib. Если тесты размещены в отдельном модуле — все тестируемые функции импортируются (а в тестируемом модуле, соответственно, экспортируются). Подробнее о директивах Erlang можно прочитать в документации.

Перед запуском тестов должны быть скомпилированы и тестируемый модуль, и модуль с тестами. Запуск тестов осуществляется вызовом функции eunit:test/1, примерно следующим образом:

c(task_3_8, [debug_info]). c(task_3_8_tests, [debug_info]). eunit:test(task_3_8).

Отмечу, что юнит-тесты обычно должны содержать описания «критических случаев», т.е. такие примеры исходных данных, в которых наиболее ожидаемо падение программы. Такими примерами могут быть очень большие , или наоборот пустые, строки, списки. Для нашей задачи такие примером могло бы быть выражение, не содержащие ни одного символа или выражение, содержащее пробелы и лишние скобки.

У нас не описаны такие тесты, т.к. в задаче не оговорено как в этих случаях должна вести себя программа.

На рис. 1 показано, что написанная программа успешно проходит все приведенные тесты. На рис. 2 приведен пример сообщения об ошибке (мы намеренно внесли ошибку — поставили в exp_calc оператор умножения вместо оператора сложения). Видно, что приводится исчерпывающая информация об ошибке.

рис. 1 запуск unit-тестов Erlang (без ошибок) рис. 2 запуск unit-тестов Erlang (с ошибками)

Как всегда, прикрепляю архив с исходным кодом: разбор выражений[Erlang] .

Ссылки:

https://habrahabr.ru/post/100869/ http://erlang.org/doc/apps/eunit/chapter.html#EUnit_macros Ф. Чезарини, С. Томпсон, Программирование в Erlang
Новости
Провайдеры:
  • 08.09.2015

    Batyevka.NET предоставляет услуги доступа к сети Интернет на территории Соломенского района г. Киева.Наша миссия —... 
    Читать полностью

  • 08.09.2015
    IPNET

    Компания IPNET — это крупнейший оператор и технологический лидер на рынке телекоммуникаций Киева. Мы предоставляем... 
    Читать полностью

  • 08.09.2015
    Boryspil.Net

    Интернет-провайдер «Boryspil.net» начал свою работу в 2008 году и на данный момент является одним из крупнейших поставщиков... 
    Читать полностью

  • 08.09.2015
    4OKNET

    Наша компания работает в сфере телекоммуникационных услуг, а именно — предоставлении доступа в сеть интернет.Уже... 
    Читать полностью

  • 08.09.2015
    Телегруп

    ДП «Телегруп-Украина» – IT-компания с 15-летним опытом работы на рынке телекоммуникационных услуг, а также официальный... 
    Читать полностью

  • 08.09.2015
    Софтлинк

    Высокая скоростьМы являемся участником Украинского центра обмена трафиком (UA — IX) с включением 10 Гбит / сек... 
    Читать полностью

rss