Задача: создать «дружелюбное» окружение над фреймворком CUnit, позволяющие разработчикам/тестерам без дополнительных телодвижений добавлять новые тесты. Почему в качестве фреймворка используется CUnit? Все просто: звезды так сошлись.
Здесь я не буду описывать как работает CUnit или как писать тест-кейсы и тест-сьюты с использованием данного фреймворка. Все это есть в официальной документации, которая расположена по адресу http://cunit.sourceforge.net/doc/index.html.
Итак, вначале определимся со структурой каталогов и файлов:
Каждый тест-сьют будет расположен в отдельном файле в каталоге suites. Задача разработчика или тестировщика только написать тест-сьют и положить его в папку suites. Других телодвижений от разработчика/тестера не требуется, тест-сьют будет автоматически подхвачен системой сборки для компиляции, а потом собственно и исполняемой программой при запуске тестов.
После сборки на выходе мы должны получить runtests — исполняемая программа и модули с тест-сьютами.
Договоримся, что тест-кейсы будут иметь префикс test_. То есть если мы тестируем библиотечную функцию foo(), то тест-кейс для функции должен называться test_foo().
В каждом динамическом модуле тест-сьюте должна быть экспортирована функция runSuite(), которая будет вызываться в исполняемой программе. В даной функции должен создаваться тест-сьют, средствами CUnit, с которым связываются тест-кейсы. Прототип функции:
void runSuite(vod);
suite1.c:
В момент запуска исполняемой программы runtests она загружает все динамические модули — тест-сьюты из каталога suites, если не задана переменная окружения TEST_MODULES_DIR, и выполняет функцию runSuite() каждого модуля. Если указана переменная среды окружения TEST_MODULES_DIR, то модули будут загружаться из каталога на который указывает эта переменная
Первым дело реализуем основную программу и вспомогательную функцию поиска динамических модулей. Функции будут реализованы в файле main.c:
Для того чтобы постоянно вручную не писать префикс у тест-кейсов или, чего хуже, если в последующем префикс будет изменен, не переименовывать все тест-кейсы напишем вспомогательный макрос TEST_FUNCT:
Теперь вместо того чтобы писать:
пишем:
Добавим еще один макрос ADD_SUITE_TEST для добавления тест-кейсов к тест-сьюту:
Ну и последние, что нам нужно — это вспомогательная функция для создания тест-сьюта CUnitCreateSuite()
Макросы и пртотипы вспомогательных функций расположены в файл utils.h:
В файле utils.c реализуем вспомогательные функции:
Теперь напишем тест-сьют:
Файл suites/CMakeLists.txt:
Файл CMakeLists.txt:

Здесь я не буду описывать как работает CUnit или как писать тест-кейсы и тест-сьюты с использованием данного фреймворка. Все это есть в официальной документации, которая расположена по адресу http://cunit.sourceforge.net/doc/index.html.
Итак, вначале определимся со структурой каталогов и файлов:
.tests
|-- suites
| |-- CMakeLists.txt
| |-- suite1.c
| |-- suite2.c
| |-- suite3.c
|-- main.c
|-- utils.c
|-- utils.h
|-- CMakeLists.txt
Каждый тест-сьют будет расположен в отдельном файле в каталоге suites. Задача разработчика или тестировщика только написать тест-сьют и положить его в папку suites. Других телодвижений от разработчика/тестера не требуется, тест-сьют будет автоматически подхвачен системой сборки для компиляции, а потом собственно и исполняемой программой при запуске тестов.
После сборки на выходе мы должны получить runtests — исполняемая программа и модули с тест-сьютами.
Соглашение о наименовании функций
Договоримся, что тест-кейсы будут иметь префикс test_. То есть если мы тестируем библиотечную функцию foo(), то тест-кейс для функции должен называться test_foo().
В каждом динамическом модуле тест-сьюте должна быть экспортирована функция runSuite(), которая будет вызываться в исполняемой программе. В даной функции должен создаваться тест-сьют, средствами CUnit, с которым связываются тест-кейсы. Прототип функции:
void runSuite(vod);
Шаблон динамического модуля — тест-сьют
suite1.c:
/* Первый тест-кейс */
static void test_foo(void) {
/* Код тест-кейса */
}
/* Второй тест-кейс */
static void test_foo2(void) {
/* Код тест-кейса */
}
void runSuite(void) {
/* Код тест-сьюта */
}
Как это должно работать
В момент запуска исполняемой программы runtests она загружает все динамические модули — тест-сьюты из каталога suites, если не задана переменная окружения TEST_MODULES_DIR, и выполняет функцию runSuite() каждого модуля. Если указана переменная среды окружения TEST_MODULES_DIR, то модули будут загружаться из каталога на который указывает эта переменная
Реализация
Первым дело реализуем основную программу и вспомогательную функцию поиска динамических модулей. Функции будут реализованы в файле main.c:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <dlfcn.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <CUnit/Basic.h>
#include "utils.h"
int modules_alphasort(const char **a, const char **b) {
return strcoll(*a, *b);
}
void (*runSuite)( void);
/* Функция поиска динамических модулей */
size_t searchModulesInDir( char ***m_list, char *dir ) {
DIR *modules_dir = NULL;
struct dirent *ent = NULL;
size_t count = 0;
char **modules_list = NULL;
char *error = NULL;
void *mem_module = NULL;
void *module_handle = NULL;
unsigned int allocated_mem = 0;
errno = 0;
if( !dir ) {
return -1;
}
modules_dir = opendir( dir );
if( !modules_dir ) {
fprintf( stderr, "%s: %s\n", dir, strerror(errno));
return -1;
}
while( ( ent = readdir( modules_dir ) ) ) {
if( strncmp( ent->d_name, ".", 1 ) == 0 || strstr( ent->d_name, ".so" ) == NULL ) {
continue;
}
size_t mem_len = ( strlen( ent->d_name ) + strlen( dir ) ) * sizeof( char ) + 2;
char *module_path = malloc( mem_len );
memset(module_path, 0, mem_len);
if( !module_path ) {
fprintf( stderr, "%s\n", strerror(errno) );
return -1;
}
strncat( module_path, dir, strlen( dir ) * sizeof( char ) );
strncat( module_path, "/", 1 );
strncat( module_path, ent->d_name, strlen( ent->d_name ) * sizeof( char ) );
module_handle = dlopen ( module_path, RTLD_LAZY );
if( !module_handle ) {
fprintf( stderr, "Could not load module: '%s'\n", dlerror());
free( module_path );
continue;
}
dlerror();
runSuite= dlsym( module_handle, "runSuite" );
error = dlerror();
if( error ) {
fprintf( stderr, "Could not load module: %s\n", error);
dlclose( module_handle );
free( module_path );
continue;
}
mem_module = realloc( modules_list, allocated_mem + strlen(module_path));
allocated_mem += strlen(module_path);
if( !mem_module ) {
fprintf( stderr, "%s\n", strerror(errno));
free( module_path );
dlclose( module_handle );
return -1;
}
modules_list = mem_module;
modules_list[ count ] = module_path;
count++;
dlclose( module_handle );
}
closedir( modules_dir );
qsort(modules_list, count, sizeof(char *), (int (*)(const void *, const void *))modules_alphasort);
*m_list = modules_list;
return count;
}
int main() {
char *modules_dir = NULL;
char *env_modules_dir = NULL;
struct stat dir_info;
size_t modules_total = 0;
char **modules = NULL;
size_t i = 0;
void *module_handle = NULL;
env_modules_dir = getenv( "TEST_SUITES_DIR" );
modules_dir = ( env_modules_dir ) ? env_modules_dir : "./suites";
if( stat( modules_dir, &dir_info ) < 0 ) {
fprintf( stderr, "%s: %s\n", modules_dir, strerror(errno));
return 1;
}
if( !S_ISDIR( dir_info.st_mode ) ) {
fprintf( stderr, "'%s' is not a directory\n", modules_dir);
return 1;
}
if( access( modules_dir, R_OK | X_OK ) != 0 ) {
fprintf( stderr, "Directory '%s' is not accessible\n", modules_dir );
return 1;
}
modules_total = searchModulesInDir( &modules, modules_dir);
if(modules_total <= 0) {
fprintf( stderr, "No test suites\n");
return 0;
}
CUnitInitialize();
for( i = 0; i < modules_total; i++ ) {
module_handle = dlopen ( modules[i], RTLD_LAZY );
if( !module_handle ) {
fprintf( stderr, "Module '%s'\n", dlerror());
continue;
}
runSuite = dlsym( module_handle, "runSuite" );
runSuite();
}
CU_basic_set_mode(CU_BRM_VERBOSE);
CU_basic_run_tests();
CUnitUInitialize();
return CU_get_error();
}
Вспомогательные функции и макросы окружения
Для того чтобы постоянно вручную не писать префикс у тест-кейсов или, чего хуже, если в последующем префикс будет изменен, не переименовывать все тест-кейсы напишем вспомогательный макрос TEST_FUNCT:
#define TEST_FUNCT(name) \
static void test_##name()
Теперь вместо того чтобы писать:
static void test_foo() {
/* Some code */
}
пишем:
TEST_FUNCT(foo) {
/* Some code */
}
Добавим еще один макрос ADD_SUITE_TEST для добавления тест-кейсов к тест-сьюту:
#define ADD_SUITE_TEST(suite, name) \
if ((NULL == CU_add_test(suite, #name, (CU_TestFunc)test_##name))) {\
CU_cleanup_registry();\
return;\
}\
Ну и последние, что нам нужно — это вспомогательная функция для создания тест-сьюта CUnitCreateSuite()
Макросы и пртотипы вспомогательных функций расположены в файл utils.h:
#ifndef __UTILS_H__
#define __UTILS_H__
#include <stdio.h>
#include <stdlib.h>
#include <CUnit/Basic.h>
#define TEST_FUNCT(name) \
static void test_##name()
#define ADD_SUITE_TEST(suite, name) \
if ((NULL == CU_add_test(suite, #name, (CU_TestFunc)test_##name))) {\
CU_cleanup_registry();\
return;\
}\
CU_pSuite CUnitCreateSuite(const char* title);
void CUnitInitialize(void);
void CUnitUInitialize(void);
#endif
В файле utils.c реализуем вспомогательные функции:
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <CUnit/Basic.h>
#include "utils.h"
void CUnitUInitialize(void)
{
CU_cleanup_registry();
}
void CUnitInitialize(void)
{
if (CU_initialize_registry() != CUE_SUCCESS) {
fprintf(stderr, "Failed to initialize the CUnit registry: %d\n", CU_get_error());
exit(1);
}
}
static int initSuite(void) {
return 0;
}
static int cleanSuite(void) {
return 0;
}
CU_pSuite CUnitCreateSuite(const char* title)
{
CU_pSuite suite = NULL;
suite = CU_add_suite(title, initSuite, cleanSuite);
if (suite == NULL) {
CU_cleanup_registry();
return NULL;
}
return suite;
}
Теперь напишем тест-сьют:
#include <stdio.h>
#include <stdlib.h>
#include <CUnit/Basic.h>
#include "utils.h"
TEST_FUNCT(foo) {
/* Фейковый код */
CU_ASSERT_EQUAL(0, 1);
}
TEST_FUNCT(foo2) {
/* Фейковый код */
CU_ASSERT_EQUAL(1, 1);
}
void runSuite(void) {
CU_pSuite suite = CUnitCreateSuite("Suite1");
if (suite) {
ADD_SUITE_TEST(suite, foo)
ADD_SUITE_TEST(suite, foo2)
}
}
Сборка
Файл suites/CMakeLists.txt:
MACRO(ADD_MODULE file)
ADD_LIBRARY( ${file} MODULE ${file}.c ../utils.c )
TARGET_LINK_LIBRARIES( ${file} cunit )
SET_TARGET_PROPERTIES( ${file} PROPERTIES
PREFIX ""
LIBRARY_OUTPUT_DIRECTORY "."
)
ENDMACRO(ADD_MODULE file)
FILE(GLOB C_FILES RELATIVE "${CMAKE_SOURCE_DIR}/suites" "${CMAKE_SOURCE_DIR}/suites/*.c")
INCLUDE_DIRECTORIES ( "${CMAKE_SOURCE_DIR}" )
FOREACH ( module ${C_FILES} )
STRING( REGEX REPLACE ".c$" "" module "${module}" )
MESSAGE(STATUS "Found test suite: ${module}")
ADD_MODULE(${module})
ENDFOREACH ( module ${MODULES} )
Файл CMakeLists.txt:
CMAKE_MINIMUM_REQUIRED (VERSION 2.6)
SET(CMAKE_VERBOSE_MAKEFILE ON)
PROJECT("runtest")
SET(CMAKE_C_FLAGS " -std=c99 -O3 -Wall -Wextra -Wimplicit")
INCLUDE_DIRECTORIES ( "/usr/include" )
ADD_EXECUTABLE(runtests main.c utils.c)
TARGET_LINK_LIBRARIES(runtests cunit dl)
ADD_CUSTOM_TARGET(test "./runtests" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" VERBATIM)
ADD_SUBDIRECTORY(suites)
antonio:tests antonio$ cmake . antonio:tests antonio$ make antonio:tests antonio$ ./runtests
