Skip to content

Instantly share code, notes, and snippets.

@ProExpertProg
Created July 31, 2020 03:43
Show Gist options
  • Save ProExpertProg/f7f65480a6395a3a749f7e572eb21808 to your computer and use it in GitHub Desktop.
Save ProExpertProg/f7f65480a6395a3a749f7e572eb21808 to your computer and use it in GitHub Desktop.

THIS IS STILL A DRAFT - come back soon for an updated version. It has not been tested, just an idea for now.

Abstract

GoogleTest is one of the most popular testing frameworks in C++. It also includes GoogleMock, a comprehensive mocking framework.

GoogleMock works by extending the class we are trying to mock and providing own implementations of the methods. However, because of that, it does not allow mocking of nonvirtual methods. The GoogleTest guide recommends coding to interfaces, where we mock interfaces rather than concrete implementations. That can be a good approach in massive enterprise applications, however, C++ is often chosen for performance-critical reasons. In such cases, we cannot afford to make all public functions virtual

  • there are also other drawback which I descibe in more detail below.

What we can do is separate each unit test into its own target, which is very straightforward with CMake. CTest and GoogleTest also have good support for running tests in separate executable targets. Then, we provide a "stubbed" version of the implementation of the class, which delegates method calls to a twin object. We handle both construction and destruction of twin mocks inside the constructor of the stubbed class, as we now have the freedom to implement the constructors.

Problem

Code under test

We are testing class UnderTest and mocking class Dep. We have the following structure:

UnderTest.h:

#include "Dep.h"

class UnderTest {
  private:
  Dep dep;
  
  public:
  UnderTest();
  
  int method();

}

UnderTest.cpp:

#include "UnderTest.h"

UnderTest::UnderTest() {
  
}


int UnderTest::method() {
  return dep.anotherMethod();
}

Dep.h:

class Dep {
  public:
  Dep();
  
  int anotherMethod();

}

The currently proposed way

We have to use dependency injection. If we still want UnderTest to be responsible for Dep's construction, we need to provide two constructors. UnderTest.h:

#include "Dep.h"

class UnderTest {
  private:
  Dep *dep; // notice how this is now a pointer.
  
  public:
  UnderTest();
  explicit UnderTest(Dep *dep);
  
  int method();

}

UnderTest.cpp:

#include "UnderTest.h"

UnderTest::UnderTest() : UnderTest(new Dep) {}
UnderTest::UnderTest(Dep *dep) : dep(dep) {}
UnderTest::~UnderTest() {
  delete dep; // potential source of bugs if the programmer forgets to delete it
}


int UnderTest::method() {
  return dep.anotherMethod();
}

Dep.h:

class Dep {
  public:
  Dep();
  
  virtual int anotherMethod();

}

test-under-test.cpp

class MockDep : public Dep {
  MOCK_METHOD(int, anotherMethod, ());
}

TEST(TestUnderTest, Method) {
  MockDep mock;
  UnderTest obj(&mock);
  
  EXPECT_CALL(mock, anotherMethod()).WillOnce(Return(1));
  
  int res = obj.method();
  ASSERT_EQ(res, 1);
}

There are a lot of problems with this:

  • we had to modify the actual implementation code to enable better testing
  • we made a non-virtual method virtual, which shouldn't be treated lightly and becomes especially problematic if we're extending UnderTest somewhere else
  • we switched from a statically owned to an injected dependency can lead to memory leakage.
  • we had to add code to UnderTest, making it possible for users of the class to use it incorrectly (by using the wrong constructor).

A possible solution to the second problem is to replace virtual with a macro that resolves to virtual in testing and in production code. However, this isn't the cleanest solution and still leaves us with the other problems listed.

Compile-time substitution and twin mocks

If we compile these files into a standalone test executable, could we replace the implementation of Dep with a mocked one without modifying the header file? Turns out we can, but with a few caveats:

  • We could technically replace the header file as well by replacing the include path. However, it is likely that the header path is specified as a relative path, in which case we can't really do anything. Additionally, it prevents us from checking that the mock and the actual interface are in sync during compilation
  • If we don't replace the header file, we cannot add our own properties. So, it will take a bit more effort to add mocking functionality to a class we can't really extend.
  • This will reduce the changes needed in the code we're testing to 0 (except for not being able to define public methods in the header - how to deal with templates TBD), however, it will significantly increase the code needed for testing. We think this is a reasonable tradeoff in performance-critical applications.
  • The mocks might now get constructed inside the class, so we might need to use (this)[https://github.com/martong/access_private] to extract them.
  • For dependencies that get injected through pointers, we don't need to use the above trick to get access to private members. The stub implementation is still required, and we get the same benefits as compared to the coding to interfaces described above.

The code under test stays the same. So does the MockDep.h. However, we now need the following stub implementation:

Dep.cpp.stub

// This is a proof-of-concept. It wouldn't compile with multiple stubs
// and common code should be extracted into a header in the real implementation
using namespace std;

map<Dep*, MockDep*> mocks;

Dep::Dep() {
  mocks.insert(pair<>(this, new MockDep));
}

Dep::~Dep() {
  auto it = mocks.find(this);
  if (it == mocks.end()) {
    throw "mock does not exist";
  }
  delete *it;
  mocks.erase(it);
}

int Dep::anotherMethod() {
  auto it = mocks.find(this);
  if (it == mocks.end()) {
    throw "mock does not exist";
  }
  return it->anotherMethod();
}

// and so on for all the other methods

test-under-test.cpp

// from the helper library linked above
ACCESS_PRIVATE_FIELD(UnderTest, Dep, dep);

TEST(TestUnderTest, Method) {
  UnderTest obj;
  Dep *dep = &access_private::dep(&obj);
  
  // NEED TO INCLUDE THE getMock CALL BUT YOU GET THE IDEA
  EXPECT_CALL(getMock(dep), anotherMethod()).WillOnce(Return(1));
  
  int res = obj.method();
  ASSERT_EQ(res, 1);
}

The common code could be extracted to a header file, and the .cpp.stub files could be generated from header files we mock.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment