Parts
- C++ Mock Library: Part 1
- C++ Mock Library: Part 2
- C++ Mock Library: Part 3
- C++ Mock Library: Part 4
- C++ Mock Library: Part 5
- C++ Mock Library: Part 6
Removing redundant MOCK_SYS usage
After writing a bunch of unit test we see a pattern emerge. The lambda's we are using don't actually do much and most of the time simply return the same value. We only need to specialize them when testing error cases (most of the time they return the same thing).
So part two is about building an object that provides a default implementation for all the mocks.
Building: test/MockHeaderInclude.h
We build the following as part of the build files.
test/MockHeaderInclude.h: .FORCE
buildMockHeaderInclude
Then the script that does the work:
#!/bin/bash
IFS='%' read -r -d '' -a fileTemplate <<PREFIX
#ifndef THORSANVIl_THORS_SOCKET_MOCK_HEADER_INCLUDE
#define THORSANVIl_THORS_SOCKET_MOCK_HEADER_INCLUDE
#include <functional>
// Please add includes for all mocked libraries here.
// PART-1-Start
%
// PART-1-End
namespace ThorsAnvil::BuildTools::Mock
{
// Please define all FuncType_<XXX> here
// There should be one for each MOCK_TFUNC you use in the code.
// The make files will provide the declaration but these need to be filled in by
// the developer and committed to source control
// PART-2-Start
%
// PART-2-End
// This default implementation of overridden functions
// Please provide a lambda for the implementation
// When you add/remove a MOCK_FUNC or MOCK_TFUNC to the source
// This list will be updated.
}
#include "coverage/MockHeaders.h"
namespace ThorsAnvil::BuildTools::Mock
{
class MockAllDefaultFunctions
{
int version;
// PART-3-Start
%
// PART-3-End
public:
MockAllDefaultFunctions()
: version(2)
// PART-4-Start
%
// PART-4-End
{}
};
}
#endif
PREFIX
function copyPart {
local fileName=1ドル
local section=2ドル
cat ${fileName} | awk 'BEGIN {InSection=0;} /'PART-${section}'-Start/ {InSection=1;next;} /PART-'${section}'-End/ {InSection=0} {if (InSection == 1){print}}'
}
function getFunctions {
perl -ne '/MOCK_(T?)FUNC\([ \t]*([^\) \t]*)/ and print "2ドル 1ドル\n"' * | sort | uniq
}
function buildFuncType {
local fileName=1ドル
while read line; do
split=(${line})
name=${split[0]}
type=${split[1]}
if [[ "${type}" == "T" ]]; then
find=$(grep "using FuncType_${name}[ \t]*=" "${fileName}")
if [[ "${find}" == "" ]]; then
echo "using FuncType_${name} = /* Add function type info here */;"
fi
fi
done < <(getFunctions)
}
function buildMEMBER {
local fileName=1ドル
while read line; do
split=(${line})
name=${split[0]}
type=${split[1]}
find=$(grep "MOCK_${type}MEMBER(${name});" "${fileName}")
if [[ "${find}" == "" ]]; then
echo " MOCK_${type}MEMBER(${name});"
fi
done < <(getFunctions)
}
function buildPARAM {
local fileName=1ドル
while read line; do
split=(${line})
name=${split[0]}
find=$(grep " MOCK_PARAM(${name}," "${fileName}")
if [[ "${find}" == "" ]]; then
echo " , MOCK_PARAM(${name}, []( Add expected parameters here ){return Add default value here;}),"
fi
done < <(getFunctions)
}
function createFile {
local fileName=1ドル
echo "${fileTemplate[0]}"
copyPart "${fileName}" 1
echo "${fileTemplate[1]}"
copyPart "${fileName}" 2
buildFuncType ${fileName}
echo "${fileTemplate[2]}"
copyPart "${fileName}" 3
buildMEMBER ${fileName}
echo "${fileTemplate[3]}"
copyPart "${fileName}" 4
buildPARAM ${fileName}
echo "${fileTemplate[4]}"
}
function buildFile {
local fileName=1ドル
if [[ -e test ]]; then
createFile ${fileName} > ${fileName}.tmp
if [[ -e ${fileName} ]]; then
diff ${fileName}.tmp ${fileName}
if [[ $? == 1 ]]; then
echo "ReBuilt: ${fileName}"
mv ${fileName}.tmp ${fileName}
else \
rm ${fileName}.tmp
fi
else \
echo "Built: ${fileName}"
mv ${fileName}.tmp ${fileName}
fi
fi
}
buildFile test/MockHeaderInclude.h
This then generates the following file:
#ifndef THORSANVIl_THORS_SOCKET_MOCK_HEADER_INCLUDE
#define THORSANVIl_THORS_SOCKET_MOCK_HEADER_INCLUDE
#include <functional>
// Please add includes for all mocked libraries here.
// PART-1-Start
// PART-1-End
namespace ThorsAnvil::BuildTools::Mock
{
// Please define all FuncType_<XXX> here
// There should be one for each MOCK_TFUNC you use in the code.
// The make files will provide the declaration but these need to be filled in by
// the developer and committed to source control
// PART-2-Start
// PART-2-End
// This default implementation of overridden functions
// Please provide a lambda for the implementation
// When you add/remove a MOCK_FUNC or MOCK_TFUNC to the source
// This list will be updated.
}
#include "coverage/MockHeaders.h"
namespace ThorsAnvil::BuildTools::Mock
{
class MockAllDefaultFunctions
{
int version;
// PART-3-Start
MOCK_MEMBER(close);
MOCK_MEMBER(connect);
MOCK_TMEMBER(fcntl);
MOCK_MEMBER(gethostbyname);
MOCK_TMEMBER(open);
MOCK_MEMBER(pipe);
MOCK_MEMBER(read);
MOCK_MEMBER(shutdown);
MOCK_MEMBER(socket);
MOCK_MEMBER(write);
// PART-3-End
public:
MockAllDefaultFunctions()
: version(2)
// PART-4-Start
, MOCK_PARAM(close, []( Add expected parameters here ){return Add default value here;}),
, MOCK_PARAM(connect, []( Add expected parameters here ){return Add default value here;}),
, MOCK_PARAM(fcntl, []( Add expected parameters here ){return Add default value here;}),
, MOCK_PARAM(gethostbyname, []( Add expected parameters here ){return Add default value here;}),
, MOCK_PARAM(open, []( Add expected parameters here ){return Add default value here;}),
, MOCK_PARAM(pipe, []( Add expected parameters here ){return Add default value here;}),
, MOCK_PARAM(read, []( Add expected parameters here ){return Add default value here;}),
, MOCK_PARAM(shutdown, []( Add expected parameters here ){return Add default value here;}),
, MOCK_PARAM(socket, []( Add expected parameters here ){return Add default value here;}),
, MOCK_PARAM(write, []( Add expected parameters here ){return Add default value here;}),
// PART-4-End
{}
};
}
#endif
Which I then defined like this:
#ifndef THORSANVIl_THORS_SOCKET_MOCK_HEADER_INCLUDE
#define THORSANVIl_THORS_SOCKET_MOCK_HEADER_INCLUDE
#include <functional>
// Please add includes for all mocked libraries here.
// PART-1-Start
#include <fcntl.h>
#include <netdb.h>
#include "ThorsSocketConfig.h"
// PART-1-End
namespace ThorsAnvil::BuildTools::Mock
{
// Please define all FuncType_<XXX> here
// There should be one for each MOCK_TFUNC you use in the code.
// The make files will provide the declaration but these need to be filled in by
// the developer and committed to source control
// PART-2-Start
using FuncType_open = int(const char*, int, unsigned short);
using FuncType_fcntl = int(int, int, int);
// PART-2-End
// This default implementation of overridden functions
// Please provide a lambda for the implementation
// When you add/remove a MOCK_FUNC or MOCK_TFUNC to the source
// This list will be updated.
}
#include "coverage/MockHeaders.h"
namespace ThorsAnvil::BuildTools::Mock
{
class MockAllDefaultFunctions
{
int version;
// PART-3-Start
std::function<hostent*(const char*)> getHostByNameMock =[] (char const*) {
static char* addrList[] = {""};
static hostent result {.h_length=1, .h_addr_list=addrList};
return &result;
};
MOCK_MEMBER(read);
MOCK_MEMBER(write);
MOCK_TMEMBER(open);
MOCK_MEMBER(close);
MOCK_TMEMBER(fcntl);
MOCK_MEMBER(pipe);
MOCK_MEMBER(socket);
MOCK_MEMBER(gethostbyname);
MOCK_MEMBER(connect);
MOCK_MEMBER(shutdown);
// PART-3-End
public:
MockAllDefaultFunctions()
: version(2)
// PART-4-Start
, MOCK_PARAM(read, [ ](int, void*, ssize_t size) {return size;})
, MOCK_PARAM(write, [ ](int, void const*, ssize_t size) {return size;})
, MOCK_PARAM(open, [ ](char const*, int, int) {return 12;})
, MOCK_PARAM(close, [ ](int) {return 0;})
, MOCK_PARAM(fcntl, [ ](int, int, int) {return 0;})
, MOCK_PARAM(pipe, [ ](int* p) {p[0] = 12; p[1] =13;return 0;})
, MOCK_PARAM(socket, [ ](int, int, int) {return 12;})
, MOCK_PARAM(gethostbyname, std::move(getHostByNameMock))
, MOCK_PARAM(connect, [ ](int, sockaddr const*, unsigned int) {return 0;})
, MOCK_PARAM(shutdown, [ ](int, int) {return 0;})
// PART-4-End
{}
};
}
#endif
Now subsequent calls never change this file (anything I have previously defined is left unchanged). So this file is checked into source control.
If I had more MOCK_FUNC()
macros to my source then these will of course be added to this file.
Helper Macros
#define MOCK_MEMBER_EXPAND(type, func) ThorsAnvil::BuildTools::Mock::MockOutFunction<type> mockOutFunction_ ## func
#define MOCK_TMEMBER(func) MOCK_MEMBER_EXPAND(ThorsAnvil::BuildTools::Mock::FuncType_ ## func, func)
#define MOCK_MEMBER(func) MOCK_MEMBER_EXPAND(decltype(::func), func)
#define MOCK_PARAM_EXPAND(func, name, lambda) mockOutFunction_ ## func(name, lambda)
#define MOCK_PARAM(func, lambda) MOCK_PARAM_EXPAND(func, MOCK_BUILD_MOCK_NAME(func), lambda)
So basically each member declaration expands to (using socket as example):
// MOCK_MEMBER(socket)
ThorsAnvil::BuildTools::Mock::MockOutFunction<decltype(::socket)> mockOutFunction_socket;
With the constructor initializing like this:
// MOCK_PARAM(socket, [](int, int, int){return 12;})
, mockOutFunction_socket("socket", [](int, int, int){return 12;})
MockOutFunction
This was previously defined in C++ Mock Library: Part 1
Usage in Tests
TEST(UniTestBloc, MyUnitTest)
{
MockAllDefaultFunctions defaultMocks;
auto action = [](){
ThorsSocket::Socket socket("www.google.com", 80);
// Do tests
};
ASSERT_NO_THROW(action());
}
1 Answer 1
Write it in C++
The script is something I would cobble together as well when I would start working out this problem. However, it is nice for rapid prototyping, but it's actually terrible. It's not just a script, it's specifically bash, and other shells might not be able to run that script. Furthermore, it relies on lots of external tools, most notably: awk, diff, grep, perl, sort, and uniq, some of which have to be called with tiny scripts in their own language. And useless use of cat for good measure.
It's perfectly possible to write this in a platform-independent way in C++, especially using std::filesystem
, std::format()
and std::regex
. Or alternatively, if you are depending on a heavy-handed tool like Perl anyway, rewrite it as a Perl script.
Performance
Apart from probably getting a big performance boost by rewriting it in C++ (probably even despite its poor regex implementation), one issue I noticed is that the script makes it look like it doesn't rebuild anything if nothing has changed, but it actually always rebuilds the output file, but then just diffs it against the old file to check if it was different. Consider instead checking if any of the input files have a newer timestamp than the output file. If not, then you don't have to do anything. Of course, that is something that make
could already do for you. So consider changing the Makefile
so that all the input files are part of the rule's dependency, and also pass those in as command line parameters.
-
\$\begingroup\$
Consider instead checking if any of the input files have a newer timestamp than the output file.
Normally yes. But in this case they also have a dependency on the Build Tools themselves. If the build tools are updated I need to force a rebuild. \$\endgroup\$Loki Astari– Loki Astari2023年09月16日 20:59:09 +00:00Commented Sep 16, 2023 at 20:59 -
\$\begingroup\$ That can in principle also be expressed in a make rule. \$\endgroup\$G. Sliepen– G. Sliepen2023年09月16日 21:15:45 +00:00Commented Sep 16, 2023 at 21:15
-
\$\begingroup\$ Just tried doing checking of timestamps. The problem here is that the output files depend on all the source (and header files). So any changes will force a rebuild of the
MockHeaderInclude.h
which will then force a rebuild of everything. Since we are always updating something before each build this results in rebuilding everything every time. \$\endgroup\$Loki Astari– Loki Astari2023年09月17日 20:28:05 +00:00Commented Sep 17, 2023 at 20:28 -
\$\begingroup\$ Ah, I see. So ideally you want to separate tracking whether the script ran from whether MockHeaderInclude.h was updated. This could be done by creating a dummy "stamp file" that is
touch
ed every time your script runs, and use that to compare against the timestamps of all the input files, instead of the timestamp of the script itself. That way you can still eliminate calls to your script if really nothing was changed. \$\endgroup\$G. Sliepen– G. Sliepen2023年09月18日 06:18:41 +00:00Commented Sep 18, 2023 at 6:18