meekrosoft / fff

A testing micro framework for creating function test doubles
Other
777 stars 168 forks source link

Support for creating testing seams #14

Open frankbenoit opened 8 years ago

frankbenoit commented 8 years ago

Hi

i would like to have fff to support the dynamic exchange of the functions.

Lets say i have a function foo+bar and i want to replacy it with my fakes called fooTest+barTest. Then i would like to have expanded code like this:

In case for working with unittesting, i set the #define UNIT_TESTING
header:

extern void (*foo_TestPtr)( int a );
void foo( int a );
extern int (*bar_TestPtr)( int a );
int bar( int a );

source:

void (*foo_TestPtr)( int a ) = NULL;
void foo( int a ){
    if( foo_TestPtr ){
        foo_TestPtr( a );
        return;
    }
    // ... implementation
}

int (*bar_TestPtr)( int a ) = NULL;
int bar( int a ){
    if( bar_TestPtr ){
        return bar_TestPtr( a );
    }
    // ... implementation
}

When #define UNIT_TESTING is not set, this expands to:

header:

void foo( int a );
int bar( int a );

source:

void foo( int a ){
    // ... implementation
}

int bar( int a ){
    // ... implementation
}

And this is, how i want to write it:

header:

FUNC_DECL( void, foo,( int a ));
FUNC_DECL( int, bar,( int a ));

source:

FUNC_IMPL( void, foo,( int a )){
    FUNC_VOID_REDIR( foo, (a))

    // ... implementation
}

FUNC_IMPL( int, bar,( int a )){
    FUNC_VALUE_REDIR( bar, (a))
    // ... implementation
}

To implement, this can look like this:

#ifdef UNIT_TESTING
/**
 * Declare a function normally and a function pointer with _TestPtr postfix.
 *
 * Example:
 *   int example( char* name );
 *
 * Use of the macro:
 *   FUNC_DECL(int, example, ( char* name ));
 *
 * Expands to:
 *   int example( char* name );
 *   int (*example_TestPtr)( char* name );
 */
# define FUNC_DECL(r,n,a) \
        r n a;\
        extern r (*n##_TestPtr) a
#else
# define FUNC_DECL(r,n,a) extern r n a
#endif

#ifdef UNIT_TESTING
/**
 * Define the function pointer and the function signature
 *
 * Example:
 *   int example( char* name ){ ...
 *
 * Use of the macro:
 *   FUNC_IMPL(int, example, ( char* name )) { ...
 *
 * Expands to:
 *   int (*example_TestPtr)( char* name ) = NULL;
 *   int example( char* name ) { ...
 *
 *
 */
# define FUNC_IMPL(r,n,a) \
    r (*n##_TestPtr) a = NULL;\
    r n a
#else
# define FUNC_IMPL(r,n,a) r n a
#endif

#ifdef UNIT_TESTING
/**
 * Function redirect for functions without return value.
 *
 * FUNC_IMPL(void, example, ( char* name )) {
 *      FUNC_REDIR_VOID( example, name )
 *      ...
 * }
 *
 * Expands to:
 *   void (*example_TestPtr)( char* name ) = NULL;
 *   void example( char* name ) {
 *       if( example_TestPtr ){
 *           return example_TestPtr( name );
 *       }
 */
# define FUNC_REDIR_VOID(n,a) \
    do{ if( n##_TestPtr ){\
        n##_TestPtr a;\
        return;\
    } } while( false )
#else
# define FUNC_REDIR_VOID(n,a) \
    do{ } while( false )
#endif

#ifdef UNIT_TESTING
/**
 * Function redirect for function with return value.
 *
 * FUNC_IMPL(int, example, ( char* name )) {
 *      FUNC_REDIR_VALUE( example, name )
 *      ...
 * }
 *
 * Expands to:
 *   int (*example_TestPtr)( char* name ) = NULL;
 *   int example( char* name ) {
 *       if( example_TestPtr ){
 *           example_TestPtr( name );
 *           return;
 *       }
 */
# define FUNC_REDIR_VALUE(n,a) \
    do{ if( n##_TestPtr ){\
        return n##_TestPtr a;\
    }} while( false )
#else
# define FUNC_REDIR_VALUE(n,a) \
    do{ } while( false )
#endif

This way, you have function runtime substitution, like you discussed here: https://meekrosoft.wordpress.com/2012/07/20/test-seams-in-c-function-pointers-vs-preprocessor-hash-defines-vs-link-time-substitution/ But the two contra arguments are removed. Now the IDE can resolve the function calls, do auto completion and do code analysis.

Finally, if there would be a way to make the FUNC_IMPL and FUNC_VOID_REDIR a single macro call, this would be super cool. But I don't know how to do that. Perhaps you?

Frank

usr42 commented 7 years ago

Hi Frank,

the custom_fake function pointer provided by FFF allows you to dynamically change the function used for your tests: Custom Return Value Delegate You can also use the custom_fake for void functions.

Is that what you are looking for or am I missing something?

Balthasar

frankbenoit commented 7 years ago

Perhaps I am missing something :-)

I use FFF to fake functions. But I need the pattern above to switch the sources between real-func and faked-func.

I have 3 cases:

  1. compiled production code, which shall be just the functions (UNIT_TESTING is not defined)
  2. compiled testing code, but the function is not faked (UNIT_TESTING is defined)
  3. compiled testing code, the function is faked by replacing the pointer with the FFF fake function. (UNIT_TESTING is defined)
usr42 commented 7 years ago

Ok, now I got the point. I am currently working on a solution for that, which uses gcc's linker option --wrap to wrap functions. I added additional macros to fff which generates the _wrap fakes with fff and let these fakes use the _real function by default. You can then generate fff-fakes which still use the real function. And you can still spy on the function calls and parameters. Or for some tests you can decide to don't use the real function at all, by changing the *_fake.custom_fake function pointer.

I think it is only working with gcc. At least I am not aware of a similar feature like the --wrap option for other compilers. (But I usually always use gcc, so maybe I am missing something). So when you also use gcc you could try my branch: https://github.com/usr42/fff/tree/wrap_fff It should be working (I added tests and used it for some fakes in a project), but the README in not updated yet. I'll push at least a bullet point documentation to the README to give you a starting point.

Please tell me if are going to test it. Feedback is then more than welcome!

Thanks and regards, Balthasar

espenalb commented 7 years ago

I like the gcc --wrap concept a lot. Integrating that into fff would be perfect. We cross-compile our embedded code - we use gcc for unit tests and IAR for production code. This means a little extra work, but has the added benefit of ensuring from day zero that our code can cross-compile, which helps us isolate any esoteric IAR specific code.

Do you also have a nice pattern for maintaining the --wrap instructions?

usr42 commented 7 years ago

I added a simple and not nearly perfect adaptation in the makefile for fff's tests:

WRAP_LDFLAGS += $(shell grep -r WRAP_FAKE_VALUE_FUNC ./ | grep -v "fff\.h" | grep -v "Makefile" | sed -rn 's/[^,]*,\W*(\w*).*/-Wl,--wrap=\1/p' | sort -u)
WRAP_LDFLAGS += $(shell grep -r WRAP_FAKE_VOID_FUNC ./ | grep -v "fff\.h"| grep -v "Makefile" | sed -rn 's/.*WRAP_FAKE_VOID_FUNC\w*\W*(\w*).*/-Wl,--wrap=\1/p' | sort -u)

...

$(FFF_TEST_CPP_TARGET): $(FFF_TEST_CPP_OBJS) 
    @echo 'Building target: $@'
    @echo 'Invoking: GCC C++ Linker'
    g++ -o "$(FFF_TEST_CPP_TARGET)" $(FFF_TEST_CPP_OBJS) $(LIBS) $(WRAP_LDFLAGS)

...

For details see: https://github.com/usr42/fff/blob/wrap_fff/test/Makefile

Is that answering your question?

If you would test my approach/branch and give me feedback, that would be awesome.

One additional note on the --wrap approach: You don't have to adapt the production code at all for that. No cluttering with ifdefs or something similar.

usr42 commented 7 years ago

@espenalb Have you already tested the --wrap approach? @frankbenoit Is the --wrap approach working for your purpose?