Search code examples
cmockingposixgoogletest

Looking for ways to 'mock' posix functions in C/C++ code


I am trying to find somewhat elegant ways to mock and stub function calls to the standard C library functions. While stubbing-off calls to C files of the project is easy by just linking other C files in the tests, stubbing the standard C functions is harder. They are just there when linking.

Currently, my approach is to include the code-under-test from my test.cpp file, and placing defines like this:

#include <stdio.h>
#include <gtest/gtest.h>
#include "mymocks.h"

CMockFile MockFile;
#define open MockFile.open
#define close MockFile.close
#define read MockFile.read
#include "CodeUnderTestClass.cpp"
#undef open
#undef close
#undef read

// test-class here

This is cumbersome, and sometimes I run across code that uses 'open' as member names elsewhere or causes other collisions and issues with it. There are also cases of the code needing different defines and includes than the test-code.

So are there alternatives? Some link-time tricks or runtime tricks to override standard C functions? I thought about run-time hooking the functions but that might go too far as usually binary code is loaded read-only.

My unit-tests run only on Debian-Linux with gcc on amd64. So gcc, x64 or Linux specific tricks are also welcome.

I know that rewriting all the code-under-test to use an abstracted version of the C functions is an option, but that hint is not very useful for me.


Solution

  • Use library preloading to substitute system libraries with your own. Consider following test program code, mytest.c:

    #include <stdio.h>
    #include <fcntl.h>
    #include <unistd.h>
    
    int main(void) {
        char buf[256];
        int fd = open("file", O_RDONLY);
        if (fd >= 0) {
            printf("fd == %d\n", fd);
            int r = read(fd, buf, sizeof(buf));
            write(0, buf, r);
            close(fd);
        } else {
            printf("can't open file\n");
        }
        return 0;
    }
    

    It will open a file called file from the current directory, print it's descriptor number (usually 3), read its content and then print it on the standard output (descriptor 0).

    Now here is your test library code, mock.c:

    #include <string.h>
    #include <unistd.h>
    
    int open(const char *pathname, int flags) {
        return 100;
    }
    
    int close(int fd) {
        return 0;
    }
    
    ssize_t read(int fd, void *buf, size_t count) {
        strcpy(buf, "TEST!\n");
        return 7;
    }
    

    Compile it to a shared library called mock.so:

    $ gcc -shared -fpic -o mock.so mock.c 
    

    If you compiled mytest.c to the mytest binary, run it with following command:

    $ LD_PRELOAD=./mock.so ./mytest
    

    You should see the output:

    fd == 100
    TEST!
    

    Functions defined in mock.c were preloaded and used as a first match during the dynamic linking process, hence executing your code, and not the code from the system libraries.

    Update:

    If you want to use "original" functions, you should extract them "by hand" from the proper shared library, using dlopen, dlmap and dlclose functions. Because I don't want to clutter previous example, here's the new one, the same as previous mock.c plus dynamic symbol loading stuff:

    #include <stdio.h>
    #include <dlfcn.h>
    #include <string.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <gnu/lib-names.h>
    
    // this declares this function to run before main()
    static void startup(void) __attribute__ ((constructor));
    // this declares this function to run after main()
    static void cleanup(void) __attribute__ ((destructor));
    
    static void *sDlHandler = NULL;
    
    ssize_t (*real_write)(int fd, const void *buf, size_t count) = NULL;
    
    void startup(void) {
        char *vError;
        sDlHandler = dlopen(LIBC_SO, RTLD_LAZY);
        if (sDlHandler == NULL) {
            fprintf(stderr, "%s\n", dlerror());
            exit(EXIT_FAILURE);
        }
        real_write = (ssize_t (*)(int, const void *, size_t))dlsym(sDlHandler, "write");
        vError = dlerror();
        if (vError != NULL) {
            fprintf(stderr, "%s\n", vError);
            exit(EXIT_FAILURE);
        }
    
    }
    
    void cleanup(void) {
        dlclose(sDlHandler);
    }
    
    
    int open(const char *pathname, int flags) {
        return 100;
    }
    
    int close(int fd) {
        return 0;
    }
    
    ssize_t read(int fd, void *buf, size_t count) {
        strcpy(buf, "TEST!\n");
        return 7;
    }
    
    ssize_t write(int fd, const void *buf, size_t count) {
        if (fd == 0) {
            real_write(fd, "mock: ", 6);
        }
        real_write(fd, buf, count);
        return count;
    }
    

    Compile it with:

    $ gcc -shared -fpic -o mock.so mock.c -ldl
    

    Note the -ldl at the end of the command.

    So: startup function will run before main (so you don't need to put any initialization code in your original program) and initialize real_write to be the original write function. cleanup function will run after main, so you don't need to add any "cleaning" code at the end of main function either. All the rest works exactly the same as in the previous example, with the exception of newly implemented write function. For almost all the descriptors it will work as the original, and for file descriptor 0 it will write some extra data before the original content. In that case the output of the program will be:

    $ LD_PRELOAD=./mock.so ./mytest
    fd == 100
    mock: TEST!