Некоторое время назад, дабы не заработать дистрофию головного мозга (за два-то года службы в армии) плотно занялся вопросами 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