Friday, 2 February 2018

Break dependency in c++ unit testing

Purpose:

The purpose of this post is to help you understand dependency breaking mechanisms in c++ unit testing and to run a simple unit test suite with c++ mock object. I have demonstrated a simple yet more like real time example unit test with CXXTEST framework. you can set your dev ready by setting up the CXXTEST using the introduction turorial.

Why should I care about dependency?:

You have an application and when you test a class you should only focus on that class's functions alone which help you reduce unnecessary linking and compilation issues.More header inclusion adds more complexity to your test suite.

Example:


Download the source code here and place it in you dev machine before you proceed.

Consider the following Calculator application which simply does two operations add and subtract. For every add/sub operation this application stores the operation and input arguments to DB/Cloud for history purpose. Here our Target class for unit test is Calculator.  In our example if we want to test we have to eliminate the dependency HistoryTracker class because the HistoryTracker further has dependencies with Database and Cloud service operations.


Introducing Mock Class for your dependency:


Generally unit test frameworks provides c++ class Mocking features but still in this post we will see how to write your own Mock class for your dependency class. here our Calculator depends and on IHistory interface class only not concretely depends on HistoryTracker. We can leverage on interface dependency and create a HistoryTrackerMock class which also inherits from IHistory. 
   
Now we can inject HistoryTrackerMock to Calculator without Calculator's awareness.  Since HistoryTrackerMock is a dummy it will not have any dependency with DB/Cloud. We just need to write an empty function updateOperation inorder to implement the pure virtual function in IHistory. 



Let us code and run the test suite with Mock dependency:

Hope you have downloaded the source code here  

Let me go walk through each file and explain the intention. Finally we will run the suite and check the results. 

set_env.sh

#!/bin/bash
export CXXTEST=/x/home/gpandian/cxxtest-4.3
export PATH=$PATH:$CXXTEST/bin

This will set the necessary environment variables for compilation and running the test suite.

Calculator.h

#include "iostream"
#include "IHistory.h"

using namespace std;

class Calculator
{
   public:
 Calculator(IHistory* _history);

 int add(int a, int b);
 int sub(int a,int b);
   private:
    IHistory* m_history;
};

Calculator.cpp


#include "Calculator.h"

Calculator :: Calculator(IHistory* _history)
{
  m_history = _history; 
}

int Calculator :: add(int a, int b)
{
   m_history->updateOperation("add",a,b);
   return (a+b);
}

int Calculator :: sub(int a, int b)
{
   m_history->updateOperation("sub",a,b);
   return (a-b);
}

As you can see the Calculator class has dependency with IHistory interface pointer.This dependency is injected through constructor arguement. Here the constructor accepts any derived class which inherits from IHistory class.We use this idea and inject HistoryTrackerMock instead of actual HistoryTracker instance.

IHistory.h


#pragma once
#include "string"
using namespace std;

class IHistory
{
  public:
   virtual void updateOperation(string op,int a,int b) = 0;
};

HistoryTracker.h


#include "IHistory.h"

class HistoryTracker : public IHistory
{
  public:
   void updateOperation(string operation, int a, int b);
};

HistoryTracker.cpp


#include "HistoryTracker.h"
// include DB related headers
// Complex dependancy 1
// Complex dependancy 2... so on

void HistoryTracker :: updateOperation(string operation, int a, int b )
{
  // 1. Create DB Connection
  // 2. Add a record along with operation, input values
  // 3. Check For DB return codes
  // ASSUME ITS REALLY COMPLICATED AND DEPENDANCY FULL OPERATION 
}


HistoryTracker is a class which helps in persisting the operations and data done by Calculator. It may store it in DB/Cloud or wherever hence it needs to include the headers associated with DB operations as well. Hence it adds weight to our Calculator class if used directly for unit testing.

HistoryTrackerMock.h


#include "IHistory.h"

class HistoryTrackerMock : public IHistory
{
  public:
   void updateOperation(string operation, int a, int b);
};

HistoryTrackerMock.cpp

#include "HistoryTrackerMock.h"

void HistoryTrackerMock :: updateOperation(string operation, int a, int b )
{
  // Empty Mock to break the dedpendancy in unit testing
}


Mock class we have written has nothing inside definition hence it is light weight and u can inject it to Calculator using constructor argument in test suite.

CalculatorTestSuite.h

#include "cxxtest/TestSuite.h"
#include "Calculator.h"
#include "HistoryTrackerMock.h"

class CalculatorTestSuite : public CxxTest::TestSuite
{
public:
    
    void test_add(void)
    {
       Calculator obj(new HistoryTrackerMock);

       TS_ASSERT_EQUALS( obj.add(1,4), 5);
    }
    void test_sub(void)
    {
       Calculator obj(new HistoryTrackerMock);

       TS_ASSERT_EQUALS( obj.sub(4,1), 3);
    }
};

run.sh

#!/bin/bash

echo 'Generating .cpp file for CalculatorTestSuite.h'
cxxtestgen --error-printer -o CalculatorTestSuite.cpp CalculatorTestSuite.h

echo 'Compiling CalculatorTestSuite.cpp'
g++ -o runner -I$CXXTEST CalculatorTestSuite.cpp Calculator.cpp HistoryTrackerMock.cpp

echo 'run'
./runner

Note that while compiling we have not included HistoryTracker.cpp instead we compile HistoryTrackerMock.cpp and get the runner binary

Steps to compile and run the test suite:


$. set_env.sh
$. run.sh
 Yes these two scripts will compile and run the test cases you have written. Congrats you have learned to hard wire a dependency with Mock object for unit testing. Happy coding!!💻

Video Demo: 

Screen recorded video demo on the above content.

No comments:

Post a Comment