📜 ⬆️ ⬇️

CUnit: Automatic testing with dynamic test loading

Objective: to create a “friendly” environment above the CUnit framework, allowing developers / testers to add new tests without additional gestures. Why is CUnit used as a framework? It's simple: the stars so agreed.

Here I will not describe how CUnit works or how to write test cases and test suites using this framework. All this is in the official documentation, which is located at http://cunit.sourceforge.net/doc/index.html .


So, first we decide on the structure of directories and files:
.tests |-- suites | |-- CMakeLists.txt | |-- suite1.c | |-- suite2.c | |-- suite3.c |-- main.c |-- utils.c |-- utils.h |-- CMakeLists.txt 

')
Each test suite will be located in a separate file in the suites directory. The task of the developer or tester is only to write a test suite and put it in the suites folder. There is no need for any other gestures from the developer / tester, the test suite will be automatically picked up by the build system for compilation, and then the executable program itself when running the tests.

After assembling the output, we must get runtests - an executable program and modules with test suites.

Naming convention


We agree that test cases will have the prefix test_ . That is, if we test the library function foo () , then the test case for the function should be called test_foo () .

In each dynamic module of the test suite, the runSuite () function must be exported, which will be called in the executable program. In this function, test suites should be created by means of CUnit with which test cases are associated. Prototype function:

void runSuite (vod);


Dynamic Module Template - Test Suite


suite1.c:
 /*  - */ static void test_foo(void) { /*  - */ } /*  - */ static void test_foo2(void) { /*  - */ } void runSuite(void) { /*  - */ } 



How it should work


At the time of the runtests executable program launch, it loads all dynamic modules - test suites from the suites directory, if the TEST_MODULES_DIR environment variable is not set, and executes the runSuite () function of each module. If the environment variable TEST_MODULES_DIR is specified , the modules will be loaded from the directory pointed to by this variable.

Implementation


The first thing is to implement the main program and auxiliary function of searching for dynamic modules. Functions will be implemented in the main.c file:
 #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(); } 



Auxiliary functions and environment macros


In order not to constantly manually write the prefix in test cases or, worse, if the prefix is ​​changed later, do not rename all test cases and write the auxiliary macro TEST_FUNCT :

 #define TEST_FUNCT(name) \ static void test_##name() 


Now instead of writing:

 static void test_foo() { /* Some code */ } 


write:

 TEST_FUNCT(foo) { /* Some code */ } 


Add another ADD_SUITE_TEST macro to add test cases to the test suite:
 #define ADD_SUITE_TEST(suite, name) \ if ((NULL == CU_add_test(suite, #name, (CU_TestFunc)test_##name))) {\ CU_cleanup_registry();\ return;\ }\ 


Well, the last thing we need is a helper function to create a test suite CUnitCreateSuite ()

Macros and prototypes of helper functions are located in the utils.h file:

 #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 


In the utils.c file we implement auxiliary functions:

 #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; } 


Now we will write a test 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) } } 


Assembly


File 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 file:
 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



Source: https://habr.com/ru/post/123344/


All Articles