Home > Software engineering >  Testing templated classes with functions defined in CPP file
Testing templated classes with functions defined in CPP file

Time:03-10

For a project that I am working on, I need to mock certain classes for testing to test different behaviours of functions. For testing I use gtest. Because I am working on a game, the speed and efficiency of the code is of the essence. Because of this requirement I do not want to mock my classes by using virtual functions, but I want to mock my classes with templates, so the implementation of the classes will be defined at compile time and I do not lose performance at run time. Furthermore, because I want to have the least amount of code bloat in my other header/source files I want to split my files into headers and source files, so that some of the includes can be set in the source file. This approach however comes with a couple of problems.

Because the templated functions are defined in a source file, there will need to be an explicit definition of the classes in the source file. Otherwise these templated functions will throw an 'undefined external symbol' error at compile time. This would not be a problem if I did not have two different projects, one for the game and one for testing, as I can't make an explicit definition of a mock in the test project.

I have tried a couple of solutions, but all of them have drawbacks. I will try to demonstrate what I have done with the following piece of code: (I know and use GMock, but this is an easier example)

//Game project
//Foo.h
<template class Bar>
class Foo
{
public:
   Bar bar;
   bool ExampleFunction();
}

//Foo.cpp
#include "Foo.h"
<template class Bar>
bool Foo::ExampleFunction()
{
   return bar.Func() > 10;
}

//Testing project
//BarMock.h
class BarMock
{
public:
   int Func();
   int value;
}

//BarMock.cpp
#include "BarMock.h"
Bar::Func()
{
   return value;
}

//TestFoo.cpp
#include "Foo.h"
TEST(Tests, TestExample)
{
  Foo<BarMock> mocked;
  mocked.bar.value = 100;
  ASSERT_TRUE(mocked.ExampleFunction());
}

Solution 1: Include cpp file in testing project

This is already error prone, as including a cpp file is usually a no go. But if I only include the cpp file ONCE somewhere in the testing project it will not give me the 'c function already defined' error. This in my opinion is not a solid solution (although it is the solution I am currently using), because if I do need a templated class in 2 locations of my testing project this will (almost) always give an error.
//TestFoo.cpp
#include "Foo.h"
#include "Foo.cpp" // error prone, but does compile
TEST(Tests, TestExample)
{
  Foo<BarMock> mocked;
  mocked.bar.value = 100;
  ASSERT_TRUE(mocked.ExampleFunction());
}

Solution 2: Create definitions in header file

This is less error prone, but comes with some other drawbacks. Like I have stated before I want to keep the bloat to a minimum, but with this solution I will also include all of the headers of the Foo header (say I need in Foo and include foo somewhere, then in somewhere I will also have ).
//Game project
//Foo.h
<template class Bar>
class Foo
{
public:
   Bar bar;
   bool ExampleFunction()
   {
      return bar.Func() > 10;
   }
}

//Foo.cpp removed

Solution 3: Create virtual functions for mocks

This is my least favourite option, but it should be mentioned. Like I have stated before, this comes with a runtime performance hit and I do not want to change most of my functions to virtual functions. But in this way you will not get errors.
//BarMock.h
class BarMock
{
public:
   int Func() override;
   int value;
}

//BarMock.cpp
#include "BarMock.h"
Bar::Func() override
{
   return value;
}

Which one of these options is the best? Is there any method that I have missed? I would love to hear someone's opinion about this as I could not find a 'good' solution online.

CodePudding user response:

A variation of solution #1 by renaming the files:

  • Foo.h
#pragma once // or/and header guards
<template class Bar>
class Foo
{
public:
   Bar bar;
   bool ExampleFunction();
};
  • Foo.inc (or other extension .ixx, ...)
#pragma once // or/and header guards
#include "Foo.h"
template <class Bar>
bool Foo<Bar>::ExampleFunction()
{
   return bar.Func() > 10;
}
  • Foo.cpp
#include "Foo.h"
#include "Foo.inc"
#include "Bar.h"

// explicit instantiation
template <> struct Foo<Bar>;
  • FooTest.cpp
#include "Foo.h"
#include "Foo.inc"
#include "BarMock.h"

// Testing code...
  • Related