Skip to main content

Global State Dependency Injection

Software engineers often times face problems unit testing existing legacy code that has global state dependencies. There are times when large portions of code need to be refactored so that legacy code can be properly unit tested. In this post, I will introduce a technique that allows the unit testing of legacy C code even when there are global state dependencies.

Here is a piece of code that illustrates a global state dependency:
#include "libdb.h"
#include "libuser.h"

int valid_user(const char *name) {
    if (NULL == name) {
        return EINVAL;
    }

    if (0 != db_connect()) {
        return DB_ERROR;
    }

    if (-1 == db_user_id(name)) {
        return INVALID_USER;
    }

    return SUCCESS;
}

In this example, we can see the function valid_user has a global state dependency on a database module. If we had designed this code from the beginning, we could have used dependency injection in order to unit test this function. For example, we could have written the function that took a database object to inject our dependency.

However, as we all know, we never have this luxury in legacy code. Furthermore, functions like this tend to be very pervasive; which means that a simple parameter change will result in a very large refactoring.

So, how do we go about unit testing the valid_user function? The idea is simple, we will inject our global state dependency with the help of the C pre-processor and conditional compilation. For this example, I will define UNIT_TEST to conditionally inject the dependency int the valid_user function.

Here is what a libdb.h and libdb.c files with our dependency injection would look:
[ lidb.h ]

#ifndef LIBDB_H
#define LIBDB_H

#ifdef UNIT_TEST
    typedef struct 
    {
        int fail_connection;
        int fail_lookup_user;
    }
    db_test_t;
    extern db_test_t db_test;
#endif

int db_connect();
int db_user_id(const char  *name);

#endif

[ lidb.c ]

#include "libdb.h"

#ifdef UNIT_TEST
db_test_t db_test;

int db_connect() {
    if (db_test.fail_connection) {
        return -1;
    }
    return 0;
}
int db_user_id(const char *name) {
    if (db_test.fail_lookup_user) {
        return -1;
    }
    return 100;
}
#else

//Put working code here

#endif

Now, unit testing the function valid_user becomes a simple matter of defining UNIT_TEST and injecting our global state dependencies via our global variable db_test. All we have to do now is define UNIT_TEST and modify the db_test global variable to unit test the function. Here is how to unit test our valid_user function with global state dependency injection:
#include "libuser.h"
#include "assertions.h"

int main() {

    int result = SUCCESS;
    db_test.fail_connection = 1;
    db_test.fail_lookup_user = 1;

    result = valid_user("Bob");
    assert_non_zero(result == SUCCESS "The user should be invalid.");

    result = validate_user(NULL);
    assert_non_zero(result == EINVAL, "Should fail with NULL argument.");

    db_test.fail_connection = true;
    result = valid_user("Bob");
    assert_non_zero(result == DB_ERROR, "Database connection should fail.");

    db_test.fail_lookup_user = true;
    db_test.fail_connection = false;
    result = valid_user("Bob");
    assert_non_zero(result == INVALID_USER, "The user should not exist.");

    return 0;
}