My class is all API calls. How do I get it under test?


In mature projects, it is common to have dependencies on external API services. Those API can vary from REST services over HTTP, to database clients, or simply to system calls. And usually, it happens that there is a direct communication between the API library and our system, making any idea of having unit test in place for the classes that use the API a far-fetched dream.

An excellent book to tackle this kind of situation is Working with Legacy Code by Michael C. Feathers. It is full of insights and recipes to solve common scenarios like the one we are discussing today. We have been using it at work and it has proved very valuable to us.

Recently, I found myself dealing with a part of the codebase that falls under the situation mentioned above. In the remainder of this post, we are going to break down the specific issue to solve and identify a possible solution to get the class under a test harness.

The Problem

To set up our problem context, let’s imagine that we have a class, with only static methods, that handles file-system operations, and in particular, contains a DiskFree method to determine whether we still have free space on our disk:

// FSHelper.h
class FSHelper
{
public:
    FSHelper() {}

    static ULONGLONG DiskFree();

// Rest of the class
// ...
};

And now the implementation:

// FSHelper.cpp
ULONGLONG FSHelper::DiskFree()
{
	ULARGE_INTEGER freeBytes;
	BOOL out = GetDiskFreeSpaceEx(nullptr, nullptr, nullptr, &freeBytes);

	return out == TRUE ? freeBytes.QuadPart : 0;
}

// Rest of the class
// ...

Our task is simply to add a log message with the error if the GetDiskFreeSpaceEx API call returns FALSE. An example implementation is shown below, using the Log method that is part of the FSHelper class.

// FSHelper.cpp
ULONGLONG FSHelper::DiskFree()
{
	ULARGE_INTEGER freeBytes;
	BOOL out = GetDiskFreeSpaceEx(nullptr, nullptr, nullptr, &freeBytes);

    if (out == FALSE)
    {
        Log(L"DiskFree failed with error %lu", GetLastError());
        return 0ULL;
    }

	return freeBytes.QuadPart;
}

So far, so good. We got the job done, now let’s enjoy a well-deserved break!

Not so fast, my friend.

How do we make sure it works properly? We need some kind of tests in place, either manual or automated ones. In turn, this leads us to another issue:

How can we produce a situation where we are out of disk space?

Since we directly call the GetDiskFreeSpaceEx function, chances are that we don’t have a full disk at our disposal to test that it fails.

Instead, a better solution is to simulate that the function call failed within our system. To achieve that, let’s see below how it can be done.

Solution

Before diving into the problem’s solution, let’s mention a restriction on the FSHelper class: It has only static methods and it uses the default constructor and that cannot be changed due to the sheer amount of dependencies on FSHelper across the project.

This means we cannot inject directly the API dependency in the constructor (since there is no creation of an FSHelper instance), so using a technique like Parameterize Constructor is not an option. Instead we are going to approach our solution as a mix of two well-known techniques named Extract Interface and Introduce Static Setter to decouple our FSHelper class from the underlying Windows API calls.

Interface and Wrapper Implementation

The biggest takeaway here is the fact that we are isolating the Windows API calls into its own class, i.e FSApiWrapper, while replacing the API usage across the FSHelper class with calls to FSApi instance. Later on, this will allow us to mock the API implementation for testing purposes.

// FSApi.h
#include <fileapi.h>

class FSApi
{
public:
    virtual ~FSApi() {} // avoids undefined behavior

    virtual BOOL GetDiskFreeSpaceEx(
        _In_opt_ LPCTSTR lpDirectoryName,
        _Out_opt_ PULARGE_INTEGER lpFreeBytesAvailable,
        _Out_opt_ PULARGE_INTEGER lpTotalNumberOfBytes,
        _Out_opt_ PULARGE_INTEGER lpTotalNumberOfFreeBytes
    ) = 0;
};

// Default implementation with the real API calls
class FSApiWrapper : public FSApi
{
public:
    virtual BOOL GetDiskFreeSpaceEx(
        _In_opt_ LPCTSTR lpDirectoryName,
        _Out_opt_ PULARGE_INTEGER lpFreeBytesAvailable,
        _Out_opt_ PULARGE_INTEGER lpTotalNumberOfBytes,
        _Out_opt_ PULARGE_INTEGER lpTotalNumberOfFreeBytes
    )
    {
        return ::GetDiskFreeSpaceEx(
            lpDirectoryName, lpFreeBytesAvailable,
            lpTotalNumberOfBytes, lpTotalNumberOfFreeBytes);
    }
};

Since cpp doesn’t have a real interface concept, we simulate this by creating an abstract FSApi with only pure functions. Next, the FSApiWrapper provides the implementation for FSApi by wrapping the Windows API calls within its method definitions.

Injecting into FSHelper class

Now that we have in place our API interface and implementation, it is time to refactor our existing FSHelper. For that, we need to go through some analysis first:

  1. What is the visibility of our API instance in the FSHelper class? It is private to the class. For the outside world, i.e. the rest of the project, there shouldn’t be any difference regarding the API implementation in use.
  2. What is the scope of our API instance? Is it static-based or instance-based? Since all of the methods of the FSHelper class are static, it is necessary that our new instance would be static as well.
  3. We need a way to change the static instance, so we can create a fake instance for testing purposes. How can we get it done? We don’t want our setter to be publicly available as part of the FSHelper class, so we can add a protected instance setter that can be used by a FSHelper subclass. Since this setter is going to be protected, it means that our subclass would have to call within its definition, most likely in the constructor definition.

After polishing all these details, the resulting class declaration is as follows:

// FSHelper.h
#include "FSApi.h"

class FSHelper
{
public:
    FSHelper() {}

    static ULONGLONG DiskFree();

// Rest of the class
// ...
// New code
private:
    static FSApi* sApiInstance; // pointer to API implementation

    static FSApi* ApiInstance(); // function to get a hold on the object's pointer

protected:
    void SetApiInstance(FSApi* api); // set new instance
};

And the implementation looks like:

// FSHelper.cpp
FSApi* FSHelper::sApiInstance = nullptr; // pointer definition

FSApi* FSHelper::ApiInstance()
{
    if (sApiInstance == nullptr)
        sApiInstance = new FSApiWrapper();
    return sApiInstance;
}

// overwrites previous API implementation
void FSHelper::SetApiInstance(FSApi* api)
{
    delete sApiInstance;
    sApiInstance = api;
}

ULONGLONG FSHelper::DiskFree()
{
	ULARGE_INTEGER freeBytes;

    // Delegate call to Windows API through API instance
	BOOL out = ApiInstance()->GetDiskFreeSpaceEx(
                    nullptr, nullptr, nullptr, &freeBytes
    );

    if (out == FALSE)
    {
        Log(L"DiskFree failed with error %lu", GetLastError());
        return 0ULL;
    }

	return freeBytes.QuadPart;
}
// Rest of the class implementation

With all the required refactoring in place, it is time to validate our changes. So, bring on those tests!

Testing

For our testing, we are going to use the awesome Google Test and Google Mock as our framework. They bring a lot of features to make our lives easier while testing.

I won’t dive in here about how to setup the testing environment, but let’s assume that we already have one in place.

Mocking and faking

To start, we need to create a new implementation of the FSApi interface that we can hook up into the FSHelper class to use in our tests. For that, we can use a mock implementation as shown below.

// MockFSApi.h
#include "FSApi.h"
#include "gmock/gmock.h"

class MockFSApi : public FSApi
{
public:
    // Specifies a method with 4 parameters
    // Named GetDiskFreeSpaceEx and returing BOOL
    MOCK_METHOD4(GetDiskFreeSpaceEx, BOOL(
		_In_opt_ LPCWSTR lpDirectoryName,
		_Out_opt_ PULARGE_INTEGER lpFreeBytesAvailableToCaller,
		_Out_opt_ PULARGE_INTEGER lpTotalNumberOfBytes,
		_Out_opt_ PULARGE_INTEGER lpTotalNumberOfFreeBytes
	));
};

Next, we need somehow to be able to inject our mock implementation within the FSHelper class. To achieve this, we can subclass the FSHelper class as a Fake and hook a pointer to a mock instance by calling the protected SetApiInstance method within the constructor.

// FakeFSHelper.h
#include "FSHelper.h"
#include "MockFSApi.h"

class FakeFSHelper : public FSHelper
{
public:
    MockFSApi* fsApi;

    FakeFSHelper()
    {
        fsApi = new MockFSApi();
        SetApiInstance(fsApi); // Setting our Mock instance for the tests
    }

    ~FakeFsHelper()
    {
        SetApiInstance(nullptr);
    }
};

And now for the pièce de résistance. Read the next section for a test example.

A Test Case

We are going to write a very simple test to verify that if the GetDiskFreeSpaceEx call returns FALSE then the DiskFree function should return 0.

For that we need to set a Test Case, as shown below, and create an instance of the FakeFSHelper class. Afterwards, we need to specify what should be the return value of a call to the GetDiskFreeSpaceEx function defined in the mock class; in this test, that would be FALSE.

And finally, we test our code by creating an expectation on the call to DiskFree to return 0.

#include "FakeFSHelper.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"

TEST(FileSystemTest, DiskFreeReturnsZero)
{
    FakeFSHelper helper;

    // Specifies that the function must be call during the test run
    // Also specifies the return value of the call
    EXPECT_CALL(*(helper.fsApi), GetDiskFreeSpaceEx(
                    testing::_,
                    testing::_,
                    testing::_,
                    testing::_))
        .WillOnce(testing::Return(FALSE));

    // Compare the expected value against the function
    EXPECT_EQ(0, FSHelper::DiskFree());
}

Conclusions

To conclude our analysis, we can summarize the essential elements of the post. First, we dove into a common problem in legacy code, extracting and abstracting API calls from our codebase as a first step in the process of refactoring and getting our code under test. Then, we proposed a solution combining the techniques of Extract Interface and Introduce Setter, and finally, we showed an example of a test implementation, including the use of a mock and a fake classes.

I hope it has been worth for you as it was for me to write this little piece. Maybe you learnt a little bit. Me, I did it for sure. Stay tuned for more!!

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.