CUnit: Автоматическое тестирование с динамической загрузкой тестов

Задача: создать «дружелюбное» окружение над фреймворком CUnit, позволяющие разработчикам/тестерам без дополнительных телодвижений добавлять новые тесты. Почему в качестве фреймворка используется CUnit? Все просто: звезды так сошлись.

Здесь я не буду описывать как работает 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



Поделиться публикацией
Комментарии 1
    0
    Чтобы работало и под cygwin:

    while( ( ent = readdir( modules_dir ) ) ) {
    if( strncmp( ent->d_name, ".", 1 ) == 0 ||
    ( (strstr( ent->d_name, ".so" ) == NULL) && (strstr( ent->d_name, ".dll" ) == NULL) )
    ) {
    continue;
    }

    Спасибо!

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое