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
Side Note:
I will say the processes of splitting my code up and putting it up here has been beneficial just as part of this processes. I have found 1 small bug, I have significantly improved the naming convention I used (because I though I might get some heavy criticism about it) and in describing the code I found a feature that was no longer being used and unwound it from the code reducing the complexity significantly.
This project is not finished. Once I get some more feedback :-) I will publish it on github. Maybe (I think so) this is worth expanding as separate open source project as I have not found any decent C++ mocking libraries (even Google mock is terrible).
Final Part: The odds and ends
Couple of small things that tidy up all the code.
TA_Object
This is discussed in Part3 where we create this object and then override specific functions saved in the object. Not very interesting itself but here I will provide the macros and some hints on how I use them.
The difference between MOCK_FUNC()
and MOCK_TFUNC()
(notice the T). This is because C++ type systems can't handle C veridic functions in std::function
so we have to sort of hack around that by picking a specific variant.
TA_Object
Before we go into the TA_Object itself a quick reminder of how it is used.
TEST(UniTestBloc, MyUnitTestBlocking)
{
TA_TestNoThrow([](){
ThorsSocket::Socket socket("www.google.com", 80);
})
.expectObject(Socket) // <--- This is where we specify the TA_Object for Socket class.
.run();
}
The expectations is that you have set up the expected calls with code similar to the following:
SYS_MOCK_CLASS(Socket)
.expectInitTA(socket).toReturn(12)
.expectInitTA(gethostbyname).toReturn(&result)
.expectInitTA(connect)
.expectDestTA(close);
Currently the framework does not automate the use of Socket
in the above TA_TestNoThrow
object above but that is the next part of the project to automate that so that unit tests are even simpler to write.
The actual code of the object is relatively trivial:
class TA_Object;
TA_Object build(); // Just going to default construct an object.
// We then use default move to build the object we want
// See the upcoming macros for details.
class TA_Object
{
template<typename MockObject>
friend class TA_ObjectOption;
template<typename MockObject>
friend class TA_Code;
friend class TA_Test;
ActionStore block[BlockSize];
std::vector<std::function<void()>> reset;
public:
template<typename MockObject>
TA_ObjectOption<MockObject> expectInit(MockObject& action);
template<typename MockObject>
TA_ObjectOption<MockObject> expectDest(MockObject& action);
template<typename MockObject>
TA_ObjectOption<MockObject> optional(MockObject& action);
private:
template<typename MockObject>
void saveCall(MockObject& action, BlockType blockType, std::size_t callCount, std::optional<typename MockObject::Ret>&& result, std::optional<typename MockObject::Param>&& input, Insert insert, Order order);
bool hasExtraCode() const;
void addInit(std::size_t callOrder) const;
void addDest(std::size_t callOrder) const;
void addExtraCode(std::size_t callOrder) const;
void remInit(bool checkForExtraCode, bool removeOpt) const;
void remDest(bool checkForExtraCode) const;
void remExtraCode() const;
};
// -------------------------
// TA_Object
// -------------------------
TA_Object build()
{
return {};
}
bool TA_Object::hasExtraCode() const
{
return !block[BlockType::Extra].empty();
}
void TA_Object::addInit(std::size_t callOrder) const
{
std::size_t expectedCallOrder = callOrder;
for (std::size_t loop = 0; loop < block[BlockType::Init].size(); ++loop) {
expectedCallOrder = block[BlockType::Init][loop](Adding, expectedCallOrder);
}
for (std::size_t loop = 0; loop < block[BlockType::Opt].size(); ++loop) {
block[BlockType::Opt][loop](Adding, -1);
}
for (std::size_t loop = 0; loop < reset.size(); ++loop) {
reset[loop]();
}
}
void TA_Object::addDest(std::size_t callOrder) const
{
std::size_t expectedCallOrder = callOrder;
for (std::size_t loop = 0; loop < block[BlockType::Dest].size(); ++loop) {
expectedCallOrder = block[BlockType::Dest][loop](Adding, expectedCallOrder);
}
}
void TA_Object::addExtraCode(std::size_t callOrder) const
{
std::size_t expectedCallOrder = callOrder;
for (std::size_t loop = 0; loop < block[BlockType::Extra].size(); ++loop) {
expectedCallOrder = block[BlockType::Extra][loop](AddExtra, expectedCallOrder);
}
}
void TA_Object::remInit(bool checkForExtraCode, bool removeOpt) const
{
for (std::size_t loop = 0; loop < block[BlockType::Init].size(); ++loop) {
block[BlockType::Init][loop](Cleaning, checkForExtraCode ? 0 : -1);
}
if (removeOpt)
{
for (std::size_t loop = 0; loop < block[BlockType::Opt].size(); ++loop) {
block[BlockType::Opt][loop](Cleaning, -1);
}
}
}
void TA_Object::remDest(bool checkForExtraCode) const
{
for (std::size_t loop = 0; loop < block[BlockType::Dest].size(); ++loop) {
block[BlockType::Dest][loop](Cleaning, checkForExtraCode ? 0 : -1);
}
}
void TA_Object::remExtraCode() const
{
for (std::size_t loop = 0; loop < block[BlockType::Extra].size(); ++loop) {
block[BlockType::Extra][loop](Cleaning, -1);
}
for (std::size_t loop = 0; loop < block[BlockType::Opt].size(); ++loop) {
block[BlockType::Opt][loop](Cleaning, -1);
}
}
template<typename MockObject>
TA_ObjectOption<MockObject> TA_Object::expectInit(MockObject& action)
{
return TA_ObjectOption(*this, BlockType::Init, 1, action, Required::Yes, Order::InOrder);
}
template<typename MockObject>
TA_ObjectOption<MockObject> TA_Object::expectDest(MockObject& action)
{
return TA_ObjectOption(*this, BlockType::Dest, 1, action, Required::Yes, Order::InOrder);
}
template<typename MockObject>
TA_ObjectOption<MockObject> TA_Object::optional(MockObject& action)
{
return TA_ObjectOption(*this, BlockType::Opt, -1, action, Required::No, Order::Any);
}
Macros to go with the code:
/*
* Used in conjunction with expectObjectTA() and expectCodeTA()
* See below
*/
// Join a class and method name into a single identifier.
#define MOCK_JOIN_NAME(className, method) className ## _ ## method
// Get an external declaration.
// Useful if you define the object in one test file but want to use it in another.
#define SYS_MOCK_EXTERN(name) \
namespace ThorsAnvil::BuildTools::Mock::Definition \
{ \
extern TA_Object name; \
}
// This is the main macro.
// I would like to somehow limit this to taking "Class" identifiers.
// but that is not implemented yet.
// The reason for this is I want to automate more of the work of the library
// thus limitting the identifier to match an actual class will simplify that
// in the future.
// Not sure if I can use a `static_assert()` here will be following up on that.
//
// Notice the use of the build() function to create the object.
// All modifications happen on this obejct that is then moved into the declared object.
#define SYS_MOCK_CLASS(className) \
SYS_MOCK_EXTERN(className) \
ThorsAnvil::BuildTools::Mock::TA_Object ThorsAnvil::BuildTools::Mock::Definition:: className = \
build()
// Same thing as above but setting it up for methods on a class.
#define SYS_MOCK_METHOD(className, method) \
SYS_MOCK_EXTERN(MOCK_JOIN_NAME(className, method)) \
ThorsAnvil::BuildTools::Mock::TA_Object ThorsAnvil::BuildTools::Mock::Definition:: MOCK_JOIN_NAME(className, method) = \
build()
MOCK_TFUNC
Functions like fcntl()
look like this:
int fcntl(int fd, int cmd, ... /* arg */ );
The C++ type system generates an error when you try and put that in a std::function
. So I have had to manually set up type information for these types. This does limit their use in as mock functions but is something that works for now.
I may remove this and force people to use wrappers for this type of situation ?
Any advice would help.