Некоторое время назад, дабы не заработать дистрофию головного мозга (за два-то года службы в армии) плотно занялся вопросами unit-тестирования попытавшись применить его для одной из своих доработок. Что из этого получилось описано в продолжении статьи.
Некоторое время назад, дабы не заработать дистрофию головного мозга (за два-то года службы в армии) плотно занялся вопросами unit-тестирования попытавшись применить его для одной из своих доработок. Что из этого получилось я и опишу ниже.
Чтобы потом не было вопросов сразу расставлю все точки над i, описав все имевшие место предпосылки и ограничения, дабы стало видно почему выбрано то или иное решение. Итак:
1.Поскольку проект кроссплатформенный, то тесты должны запускаться как минимум на тех же платформах что и проект. Плюсом будет запуск на большем количестве платформ нежели заявлено для проекта.
2.Проект написан на C++.
3.Unit-тесты дожны быть просты в написании.
4.Необходим некий механизм пакетного запуска автотестов с генерацией простеньких отчетов по отработавшим автотестам.
Немного в общем-то требований )).
Исходя из всего вышеперечисленного было принято решение писать все unit-тесты на Питоне. Создавать такие тесты будет достаточно просто + тесты будут кроссплатформенными (пока что Питон поддержан на большем числе платформ чем мой проект). На питоне же было решено писать и тулзу для покетного запуска тестов.
Тестирование осуществляется следующим образом — программист (т.е. я) после написания кода, требующего тестирования, создает из этого кода dllшку из которой экспортируются все подлежащие тестированию функции. Делается это с помощью набора макросов (файл testin_utilities.h).
BEGIN_TESTING_SECTION() - открытие секции экспортируемых функций
END_TESTING_SECTION() - закрытие секции экспортируемых функций
FUNCTION_TESTING_1( FUNCTION_NAME , EXPORT_FUNCTION_ALIAS , PARAM_1_TYPE , FUNCTION_RET_RYPE , ALIAS_RET_TYPE ) - макрос создает экспортируемую функцию с именем EXPORT_FUNCTION_ALIAS у которой один параметр типа PARAM_1_TYPE. По сути, этот макрос создает просто обертку для функции с именем FUNCTION_NAME, которую и нужно протестировать. Аналогичных макросов наделано для функций с 0, 2 и 3 параметрами (пока больше не надо было).
FUNCTION_RET_RYPE – это тип значения, возвращаемого функцией FUNCTION_NAME.
ALIAS_RET_TYPE – это тип значения, возвращаемого экспортируемой функцией. Вообще говоря эти типы могут отличаться. Если преполагается, что экспортируемая функция не будет ничего возвращать, то в качестве этого параметра должно быть передано NO_RET. Однако эти макросы не позволяют экспортировать функции-члены каких-либо классов, хотя они так же требуют тестирования. Чтобы это сделать нужно выполнить предварительные настройки следующим макросом:
ENABLE_CLASS_TESTING( CLASS_NAME ) - где CLASS_NAME есть имя класса, требующего тестирования. Этот макрос создаст мэнэджер именованых объектов (см. файл object_manager.h) класса CLASS_NAME + функцию void CreateObject( const char * ObjectName ) для создания именованных объектов внутри этого мэнэджера. Эта функция также является экспортируемой. Вот собственно и все, вызов методов класса осуществляется похожими на макросами FUNCTION_TESTING_[0123]:
CLASS_MEMBER_FUNCTION_TESTING_1 — в этот макрос первым параметром передается имя класса, чей метод мы тестируем. Макрос создает экспортируемую функцию-враппер, которая помимо параметров тестируемой функции принимает название объекта, посредством которого будет осуществляться вызов функции-члена. Параметры макроса практически аналогичны параметрам макросов FUNCTION_TESTING_[0123]. Единственное отличие заключается в том что первым параметром дополнительно передается имя класса, чей метод будет тестироваться. Однако такой механизм не позволит тестировать закрытые функции-члены. Чтобы обойти это, нужно сделать экспортируемую функцию другом класса. Делается это с помощью макросов SET_FRIENDSHIP_[0123] в объявлении класса.
Все в общем-то просто, поэтому перейдем к примеру:
в файле test.h
class Foo{ int f1( int i ); // будет экспортироваться функция tstf1 SET_FRIENDSHIP_1R( tstf1 , int , void ) public: int f2( int i ); static void f3( void ); };
в файле test.cpp
// тут идет определение функций // здесь можно вставить секцию для unit-тестирования BEGIN_TESTING_SECTION() ENABLE_CLASS_TESTING( Foo ) // тестирование функции f1 CLASS_MEMBER_FUNCTION_TESTING_1( Foo, f1, tstf1, int, int, int ) // тестирование функции f2 CLASS_MEMBER_FUNCTION_TESTING_1R( Foo, f2, tstf1, int, int , int ) // тестирование функции f3 // функция f3 ничего не возвращает FUNCTION_TESTING_1( Foo::f3, tstf3 , void , NO_RET ) END_TESTING_SECTION()
Теперь все это компилируем в dllшку и пишем unit-тест.
Unit-тест представляет из себя скрипт на питоне, в котором через ctypes загружается либка, и дёргаются экспортируемые функции, так или иначе участвующие в тестировании.
Содержание примерно следующее (пусть файл назвается foo_test.py):
#!/usr/python # -*- coding: cp1251 -*- from base import * import time # создаем тестовые данные foo = file.LoadModule( "foo" ); foo.CreateObject( "default" ); foo.tstf1() foo.tstf2( 1 ); # запускаем тестовый стенд и проверяем результат, с которым стенд отработал if( foo.tstf3( 2 ) == 0 ): print( "TEST PASSED" ); else: print( "ERROR" );
В общем-то ничего сложного. Единственное на что здесь стоит обратить внимание — так это на вывод print( "ERROR" ). Дело в том что определение того корректно ли отработал unit-тест или нет, определяется наличием в stdout или stderr подстроки error (буквы этой подстроки могут быть в любом регистре). Если такой подстроки не было найдено, то автотест считается отработавшим корректно.
Для unit-теста также нужно создать манифест с информацией о нем. Он будет выглядеть примерно следующим образом:
<?xml version="1.0" encoding="Windows-1251"?> <root> <author name="user" email="Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. " dsc="Проверяем работу класса"/> <label value="foo, user"/> <timeout value="10"/> </root>
Где
name — имя того кто создал unit-тест
email — адрес электронной почты создателя unit-теста
dsc — описание тесткейса
label — список разделенных запятыми меток, связанных с этим unit-тестом (о назначении меток см. ниже)
timeout — столько времени отводится на выполнение unit-теста, если таймаут превышен, то тестирование останавливается
Теперь все это нужно положить туда где находится run.py и можно запускать процесс тестирования. Делается это следующим образом (для Windows):
python.exe run.py
Однако run.py поддерживает несколько флагов, от которых зависит его функционирование:
-help — выводит справку по доступным параметрам командной строки
-rn — имя файла отчета о тестировании может задаваться со следующими подстановками:
#now_year# - сегодняшний год
#now_month# - сегодняшний месяц
#now_day# - сегодняшний день
#now_hour# - час
#now_minute# - минута
#now_second# - секунда
если этот параметр не указан, то имя лога будет - autotest_log.html
-l <имя метки> - запускать автотесты, у которых в манифесте прописана метка <имя метки>
После того как тестирование отыграет, в той же директории появится файл с отчетом о тестировании. Его содержимое я описывать не буду — сами посмотрите.
Собственно вот пока все что у меня есть сказать вам по этому поводу. В планах есть создание утилиты, которая позволит запускать тесты через гуёвую формочку а не через командную строку, но когда осуществится этот замысел я не знаю ((.
Ссылки по теме:
Менеджер тестирования
Unit-тесты часть 1