Mocking again, as a “response” to my post about how easy is mocking in python. So this is my recommendation, of how to perform mocking in C++, reduce impact of testing code to a production code, and keep clean interfaces (everything in contradiction to dependency injection).
You need to know CMake
In case you don’t know how CMake populates target properties, please go to my previous post.
Goal
Goal is to link your production code like this:
add_executable(sanitize_file main.cpp)
target_link_libraries(sanitize_file
FileSanitizer::Lib)
Where FileSanitizer::Lib consist of:
set (file_sanitizer_sources
file_sanitizer.cpp
file_reader.cpp
)
add_library(file_sanitizer OBJECT ${file_sanitizer_sources})
add_library(FileSanitizer::Lib ALIAS ${PROJECT_NAME})
and you link your testing target like this:
add_executable(file_sanitizer_test test.cpp)
target_link_libraries(file_sanitizer_test
FileSanitizer::Lib::file_sanitizer # this is implementation
GTest::GTest
Mock::FileReader # this is mock
GTest::Main
Threads::Threads
)
So to link out test target partially with a production code and partially with a mocked implementation we need to do 2 things: 1) Define interface targets per compilation unit, 2) create a mock and declare target for it
Interface targets per compilation unit.
To create targets for file_sanitizer.cpp and file_reader.cpp we need will will create a little CMake Function:
function(DeclareInterfaceTargets prefix sources)
foreach(source_file ${${sources}})
get_filename_component(file_name ${source_file} NAME_WE)
set(target_name "alias_${file_name}_target")
add_library(${target_name} INTERFACE)
target_sources(${target_name} INTERFACE ${source_file})
target_include_directories(${target_name} INTERFACE
add_library(${prefix}::${file_name} ALIAS ${target_name})
endforeach()
endfunction()
Function takes a list of sources (cpp files only), declares INTERFACE library for each, and sets an ALIAS. So we have targets like FileSanitizer::Lib::file_sanitizer and FileSanitizer::Lib::file_reader. Now we can use those targets to link our test choosing what is to be tested, and what is to be mocked.
Creating Mock
We need to solve 2 problems. 1) Code that we want to replace is instantiated by a production code (unless you are doing dependency injection) so by default you do not have any access to our mock, and still we want to set EXPECT on a mock. So we will use a singleton pattern (i know …) to get the access, remember that you need to instantiate mock before running SUT (System Under Test).
TEST(MyTest, call_mock){
auto mock = FileReaderMock::get_instance();
EXPECT_CALL(*mock, read()).WillOnce(Return("hello"));
EXPECT_TRUE ( file_sanitizer::sanitize_file(""));
FileReaderMock::remove_instance();
}
2) You need to solve conflicting include directories. General approach in mocking is to mock the header (as in gmock tutorial), this mock will be included with by a source file that is using mocked part. For this you would need to replace production target_include_directories with a mocked one. Unfortunately there is not just one file in that folder, you have also production headers in this same place, and you do not want to replace them, you need to copy them into testing target_include_directories. So I do not recommend this way.
Instead I propose to leave production header as is, and use a fake implementation (so a cpp file) to proxy control flow into the mock, like this:
#include <file_reader.h>
#include <string>
#include <gmock/gmock.h>
#include <file_reader_mock.h> # actual mock
FileReader::FileReader(const std::string& ){
}
std::string FileReader::read(){
return FileReaderMock::get_instance()->read();
}
and mock itself:
#pragma once
#include <gmock/gmock.h>
class FileReaderMock;
static FileReaderMock* instance = nullptr;
class FileReaderMock{
public:
static FileReaderMock* get_instance(){
if (instance == nullptr)
instance = new FileReaderMock();
return instance;
}
static void remove_instance(){
if (instance != nullptr)
delete instance;
instance = nullptr;
}
MOCK_METHOD0(read, std::string());
};
Foot note.
This approach changes a bit meaning what is a testable code and what is not, … but also reduces impact of a testing to a production code. For complete code example please check modern_cmake_example on my GitHub.
Best regards
Michał