Как естественное продолжение работы над libuniset2, возник проект uniset2-testsuite. Это свой небольшой велосипед для функционального тестирования. В итоге он развился до более-менее универсального решения с «плагинами». Написан на python. Если интересно почитать, то прошу… заходите.
Основная идея тестирования, заложенная в uniset2-testsuite проста: «Подали воздействие, проверили реакцию». Эта абстрактная идея материализовалась в итоге в следующие артефакты:
Тестовый сценарий
Тестовый сценарий — это xml-файл, в котором записывается последовательность тестов. Каждый тест в свою очередь делится на «действия» (action) и проверки (check). Тесты могут вызывать другие тесты, и, более того, можно вызывать тесты, находящиеся в других файлах. Всё это позволяет строить довольно сложные и развесистые последовательности тестов, общие части выносить в отдельные файлы, вызывать их «как процедуры» и т.п. Помимо этого поддерживается механизм замен (replace), позволяющий писать шаблоны тестов с некоторыми абстрактными названиями «переменных», которые подменяются на конкретные в момент вызова теста.
Действия (action)
Поддерживается три вида действий:
- 'SET'. Выставление значения
- 'MSLEEP'. Пауза в миллисекундах
- 'SCRIPT'. Вызов внешнего скрипта
Тут особо пояснять нечего. На примерах ниже будет видно.
Проверки (check)
- '='. Проверка условия эквивалентности значения
- '!='. Проверка с отрицанием
- '> или >='. Проверка условия, что значение больше (или равно) заданному
- '< или <='. Проверка условия, что значение меньше (или равно) заданному
- 'LINK'. Ссылка на другой тест в данном файле
- 'OUTLINK'. Ссылка на другой файл
- HOLD. Проверка постоянства условия в течение заданного времени
Тут, я думаю, тоже более менее всё очевидно.
В итоге простой тестовый сценарий имеет следующий вид:
<?xml version = '1.0' encoding = 'utf-8'?>
<TestScenario>
<Config>
...
</Config>
<TestList type="uniset">
<test name="Processing" comment="Проверка работы процесса">
<action set="OnControl_S=1" comment="Подаём команду 'начать работу'"/>
<check test="CmdLoad_C=1" comment="Подана команда 'наполнение'"/>
<check test="CmdUnload_C=0" comment="Снята команда 'опустошение'"/>
<check test="Level_AS>=90" comment="Цистерна наполняется.." timeout="15000"/>
<check test="CmdLoad_C=0" comment="Снята команда 'наполнение'"/>
<check test="CmdUnload_C=1" comment="Подана команда 'опустошение'"/>
<check test="Level_AS<=10" comment="Цистерна опустошается.." timeout="15000"/>
</test>
<test name="Stopped" comment="Проверка остановки процесса">
<action set="OnControl_S=0" comment="Снимаем команду 'начать работу'"/>
<check test="CmdLoad_C=0" comment="команда 'наполнить' не меняется" holdtime="3000"/>
<check test="CmdUnload_C=0" comment="команда 'опустошить' не меняется" holdtime="3000"/>
<check test="Level_AS<=80" comment="Уровень не меняется" holdtime="10000"/>
</test>
</TestList>
</TestScenario>
Как видно из приведённого примера. Действия записываются в виде тега
<action set="..."/>
проверки записываются в виде
<check test=".."/>
Проигрыватель тестов
Раз есть сценарий, значит кто-то его должен исполнить. Поначалу было желание сделать систему, позволяющую написать свои «проигрыватели», у каждого из которых мог бы быть свой формат сценария. Но в итоге т. к. в качестве формата был выбран xml, то соответственно был написан uniset2-testsuite-xmlplayer. Консольный проигрыватель, который исполняет xml-сценарии. А дальше дело не пошло, потому-что xml-формат оказался достаточно удачным и xmlplayer-а хватает для решения всех текущих задач. Хотя в самом репозитории есть ещё uniset2-testsuite-gtkplayer это GUI-проигрыватель, написанный на python-gtk. Но его развитие в какой-то момент было заморожено за ненадобностью и он не поддерживает многих функций.
Итого у нас получилось, что есть сценарии, состоящие из действий и проверок и есть проигрыватель который исполняет сценарии. Если всё это запустить, то на выходе можно увидеть примерно такую картинку
Немного предыстории…
В начале uniset2-testsuite рассматривался исключительно в рамках работы с libuniset, в которой основная концепция — «всё есть датчик». Поэтому формат записи подразумевает оперирование идентификаторами датчиков (числовыми или именами) и проверкой только числовых значений (целочисленных). Но постепенно развиваясь, добавилась возможность взаимодействия по протоколу modbus (type=«modbus»), недавно добавилась возможность работы через snmp (type=«snmp»). И в итоге стало более очевидно, что интерфейс для тестирования можно свести всего к двум основным функциям GET и SET. Т.е. для тестирования какой-либо системы достаточно реализовать всего две функции get_value(id) и set_value(id, value). И протокол взаимодействия с тестируемой системой не важен. Просто разработчик предоставляет свою реализацию этих двух функций для взаимодействия со своей системой. Эта идея легла в основу создания аналога системы плагинов. Т.е. для реализации взаимодействия с какой-то своей тестируемой системой, достаточно реализовать специальный python интерфейс и положить его (подключить) в нужное место. Как написать свой интерфейс для тестирования будет рассказано во второй части. Пока же я опишу кратко возможности. Поскольку их много всяких, я расскажу об основных, про остальные можно почитать в документации wiki.etersoft.ru/UniSet2/testsuite
Описание возможностей
Автоматический запуск программ
Конечно хотелось бы, чтобы не просто исполнялись тестовые сценарии, а чтобы ещё и при запуске теста, запускались все необходимые для теста программы. Для этого случая в тестовом сценарии предусмотрена секция RunList.
Псевдопример:
<?xml version = '1.0' encoding = 'utf-8'?>
<TestScenario>
<RunList after_run_pause="5000">
<item after_run_pause="2000" script="./start_my_testprog1.sh" args="arg1 arg2" name="TestProg1"/>
<item script="./start_my_testprog2.sh" args="arg1 arg2" chdir="../../TestPrograms/" name="TestProg2"/>
<item script="./start_my_testprog3.sh" args="arg1 arg2" ignore_terminated="1" name="TestProg3"/>
</RunList>
<TestList>
...тесты..
</TestList>
<TestScenario>
В этой секции можно задать список того, что нужно запустить перед началом теста. Для каждой запускаемой программы указываются параметры запуска (args), программы могут лежать в других каталогах (chdir). Можно задать уникальное имя (name), которое в случае вылета программы будет выведено в логах. При этом как видно из примера, так же можно задать паузы после запуска той или иной программы или всех вместе. Можно задать параметр silent_mode=[0,1] , где 1 означает перенаправить весь вывод в /dev/null). А можно указать параметр logfilе=«filename», при котором весь вывод будет перенаправлен в указанный файл (отменяет действие silent_mode).
Все программы запускаются в фоновом режиме. По умолчанию, если какая-либо из них вдруг во время теста вылетает, тест прерывается с ошибкой. Но это можно отключить параметром ignore_terminated=«1». Все запущенные программы завершаются после прохождения теста (не важно успешного или нет). Т.е. в итоге RunList позволяет запускать необходимое окружение для тестирования, которое будет автоматически завершено после. Ну либо производить какие-то подготовительные действия перед началом теста.
Запуск скриптов при завершении
Ещё одной возможностью uniset2-testsuite является запуск указанных скриптов (программ) при успешном или провальном прохождении теста.
<?xml version="1.0" encoding="utf-8"?>
<TestScenario>
<Success>
<item script="./success.sh param1 param2"/>
<item script="./success.sh param3 param4"/>
</Success>
<Failure>
<item script="./failure.sh param1 param2"/>
<item script="./failure.sh param3 param4"/>
</Failure>
....
...
</TestScenatio>
Это можно использовать в разных случаях. Например (из очевидных):
- послать уведомление в случае провала теста.
- «Прибрать за тестом» после завершения работы
- скопировать артефакты провалившегося (или успешного) теста «в сторонку»
Теги
Когда у Вас становится много развесистых тестов, которые содержат «полное покрытие», возможно Вам в некоторых случаях не хочется прогонять их все, для того, чтобы проверить какую-то одну «логическую ветку тестов». Для решения этой задачи на помощь приходят «теги». Вы можете помечать каждый тест одним или несколькими тегами.
...
<test name="Test3" tags="#tag1#tag2">
<check test="outlink" file="Test4.xml" link="ALL"/>
</test>
<test name="Test5" tags="#tag1">
<check test="outlink" file="Test6.xml" link="ALL"/>
</test>
<test name="Test7" tags="#tag2">
<check test="outlink" file="Test8.xml" link="ALL"/>
</test>
...
И при запуске сценария можно указать, что нужно исполнить только тесты с указанными тегами --play-tags "#tag1#tag2#".
Вывод дерева тестов
Т.к. существует механизм вызова тестов из других тестов и других файлов, то при достаточно сложной структуре тестов хочется видеть, как они будут вызваны, в какой последовательности и «кто кого вызывает». Для этого существует специальная команда, выводящая дерево тестов на экран --show-test-tree
Test1
Test2
Test3
Test 4
Test 5
Test 6
Test 7
При этом если для этой команды указать--play-tags "#tag1#tag2#", то будет выведено дерево тестов с учётом тегов.
Test1 [#tag1]
Test2 [#tag2]
Test3 [#tag1]
Проверка корректности сценария
Немаловажной частью при сложной структуре тестов является проверка корректности.
Некоторые тесты могут проходить довольно длительное время. И бывает «обидно», когда через час работы теста он вываливается на какой-нибудь «опечатке» в названии. Для решения этой проблемы предусмотрено два специальных режима проверки теста:
- --check-scenario — Проверка сценария, завершающаяся при первой же ошибке
- --check-scenario-ignore-failed — Проверка сценария, c игнорированием ошибок...
В этих режимах фактического «исполнения» проверок не происходит, нет пауз и timeout-ов. Т.е. происходит просто «синтаксическая» проверка теста и параметров на корректность.
Вот так выглядит результат:
Дерево вызовов при вылете теста
Если при вылете теста хочется увидеть trace вызовов, есть специальный параметр --print-calltrace, выводящий на экран дерево вызовов от места вылета к началу. При этом есть специальный параметр, ограничивающий глубину --print-calltrace-limit N
Примерно так это выглядит:
Шаблоны тестов
Безусловно, «шаблоны» это удобная вещь. И uniset2-testsuite тоже есть простая реализация этого механизма. В данном случае, речь идёт о простом способе автоматической замены «одного» на «другое». Например, пишем:
...
<test name="Check [MyVariable]" lname="tname">
....
<check test="[MyVariable]=[MyValue]"/>
</test>
...
А потом хотим вызывать этот тест с различными параметрами в качестве MyVariable, MyValue. Легко…
...
<test name="Check Name1=100">
<check test="outlink" file="my-template-test.xml" link="lname=tname" replace="[MyVariable]:Name1,[MyValue]:100"/>
</test>
<test name="Check Name2=200">
<check test="outlink" file="my-template-test.xml" link="lname=tname" replace="[MyVariable]:Name2,[MyValue]:200"/>
</test>
...
Правила replace имеют три зоны видимости:
- глобальная — действует на протяжении всего тестирования
- уровень теста — действует на уровне конкретного теста
- уровень проверки (link или outlink) — действует на уровне вызова link или outlink
Глобальные replace прописываются в секции TestList
<TestList replace="[MyVariable]:Name2,[MyValue]:200"
Уровень теста прописывается в секции test и действует только на данный тест и все link, outlink которые используются внутри него.
<test name="MySuperTes replace="[MyVariable]:Name2,[MyValue]:200">
<check test="[MyVariable]=11"/>
<check test="Othrer=[MyValue]"/>
<check test="outlink" file="my-template-test.xml" link="lname=tname"/>
</test>
Уровень конкретной проверки прописывается в секции check и действует только на конкретный вызов link или outlink теста
<test name="Check Name2=200">
<check test="outlink" file="my-template-test.xml" link="lname=tname" replace="[MyVariable]:Name2,[MyValue]:200" />
</test>
Хочется заметить, что данный механизм замен (replace) действует не только на теги action, check, set, test, но так же и на названия тестов, их параметры и даже свойства replace во вложенных тестах.
Ну и под конец то, о чём не рассказал:
- отчёты в формате junit
- сохранение отчёта в файл
- использование нескольких типов интерфейсов в одном сценарии
- «compare» — сравнение параметров между собой, а не с числом
- игнорирование тестов при исполнении
- Как это ни странно,
вместо тысячи слов, приведу ещё и help программы
helpUsage: uniset2-testsuite-xmlplayer [--confile [configure.xml|alias@conf1.xml,alias2@conf2.xml,..] --testfile scenario.xml [..other options..] --testfile tests.xml - Test scenarion file. --test-name test1,prop2=test2,prop3=test3,... - Run tests from list. By default prop=name --ignore-run-list - Ignore <RunList> --ignore-nodes - Do not use '@node' or do not check node available for check scenario mode --default-timeout msec - Default <check timeout='..' ../>.' --default-check-pause msec - Default <check check_pause='..' ../>.' --print-calltrace - Display test call trace with test file name. If test-suite FAILED. --print-calltrace-limit N - How many recent calls to print. Default: 20. --supplier-name name - ObjectName for testsuite under which the value is stored in the SM. Default: AdminID. --check-scenario - Enable 'check scenario mode'. Ignore for all tests result. Only check parameters --check-scenario-ignore-failed - Enable 'check scenario mode'. Ignore for all tests result and checks --play-tags '#tag1#tag2#tag3..' - Play tests only with the specified tag --show-test-tree - Show tree of tests --hide-result-report - Hide result report --confile [conf.xml,alias1@conf.xml,..] - Configuration file for uniset test scenario. TestSuiteConsoleReporter (--log) -------------------------------------------- --log-show-tests - Show tests log --log-show-actions - Show actions log --log-show-result-only - Show only result report (Ignore [show-actions,show-tests]) --log-show-comments - Display all comments (test,check,action) --log-show-numline - Display line numbers --log-show-timestamp - Display the time --log-show-test-filename - Display test filename in test tree --log-show-test-comment - Display test comment --log-show-test-type - Display the test test type --log-hide-time - Hide elasped time --log-hide-msec - Hide milliseconds --log-col-comment-width val - Width for column "comment" --log-no-coloring-output - Disable colorization output --log-calltrace-disable-extended-info - Disable show calltrace extended information TestSuiteLogFileReporter (--logfile) -------------------------------------------- --logfile-name filename - Save log to file --logfile-trunc - Truncate logile --logfile-flush - flush every write TestSuiteJUnitReporter (--junit) -------------------------------------------- --junit-filename name - Save junit report to file --junit-deep val - Deep tree of test for report. '-1' - all tests
Во второй части рассмотрим как написать свой интерфейс для проверки.