3
\$\begingroup\$

Parts

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());
}
asked Sep 15, 2023 at 23:22
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

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.

answered Sep 16, 2023 at 10:15
\$\endgroup\$
4
  • \$\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\$ Commented Sep 16, 2023 at 20:59
  • \$\begingroup\$ That can in principle also be expressed in a make rule. \$\endgroup\$ Commented 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\$ Commented 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 touched 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\$ Commented Sep 18, 2023 at 6:18

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.