Reputation: 13135
I'm dependant on a c api which uses the following structure (the function names are just an example):
getRoot(FolderHandle* out)
getFirstChildFolder(FolderHandle in, FolderHandle* out)
getNextFolder(FolderH in, FolderHandle* out)
getChildFolder(FolderH in, FolderHandle* out)
getProperties(FolderH in, PropertiesH* out)
getChildFolder(FolderH in, FolderH* out)
getName(PropertiesH in, char** out)
getFile(FolderH in, FileH* out)
getNextFile(FileH in, FileH* out)
getProperties(FileH in, PropertiesH* out)
So I start by calling getRoot to get a folder handle for the root. To get the handle of the first file in the root fodler, I then call getFile() passing in the folder handle. To get the second and subsequent files on this level, I call getNextFile, passing in the previous file handle.
I've wrapped this in the form of a set of C++ interfaces as follows:
class IEntry
{
public:
...
virtual IFolder* root() = 0;
};
class IFolder
{
public:
...
typedef Iterator<IFile, FolderH, FileH> FileIterator;
virtual FileIterator filesBegin() const = 0;
virtual FileIterator filesEnd() const = 0;
};
class File
{
public:
...
virtual IProperties* properties() = 0;
};
class Properties
{
public:
...
virtual std::string name() = 0;
};
In unit tests, all I need to do is use the Google Mock implemenation of IEntry, IFolder, IFile etc and this is very convenient. Also the interfaces organise the functions from the c api in a manner which is much easier to understand and work with. An implementation of a particular interface, wraps the associated handle.
I use iterators to tie together function calls like getFile and getNextFile, which in this case iterate over the files in a folder. There are many such pairs of functions in the api, so I use a template class called Iterator to create my C++ style iterators.
I'm actually using std::shared_ptrs, not ordinary pointers.
So here is an example of a unit test:
std::string a(IEntry& e)
{
std::shared_ptr<IFolder> f = e.root();
return f->properties()->name();
}
TEST (FooTest, a)
{
MockEntry e;
std::shared_ptr<MockFolder> f(new MockFolder());
std::shared_ptr<MockProperties> p(new MockProperties());
EXPECT_CALL(e, root()).WillOnce(testing::Return(f));
EXPECT_CALL(*f, properties()).WillOnce(testing::Return(p));
EXPECT_CALL(*p, name()).WillOnce(testing::Return("Root"));
EXPECT_EQ(a(e), "Root");
}
However things get trickier when it comes to the use of iterators. Here is the approach I'm using in this case:
std::string b(IEntry& e)
{
std::shared_ptr<IFolder> folder = e.root();
IFile::FileIterator i = folder->filesBegin();
if(i!=f->filesEnd())
{
return i->properties()->name();
}
else
{
return "";
}
}
TEST (FooTest, b)
{
MockEntry e;
std::shared_ptr<MockFolder> f(new MockFolder());
loadFileIteratorWithZeroItems(f);
loadFileIteratorEnd(f);
std::shared_ptr<MockProperties> p(new MockProperties());
EXPECT_CALL(e, root()).WillOnce(testing::Return(f));
EXPECT_EQ(b(e), "");
}
The test is testing the else clause. I've another two tests testing the rest of the code (one file and multiple files).
The function loadFileIteratorWithZeroItems is manipulating the internals of the iterator so that it will iterate over zero items. loadFileIteratorEnd sets up the return value from filesEnd(). Here is loadFileIteratorWithZeroItems:
void loadFileIteratorWithZeroItems (std::shared_ptr<MockFolder> folder)
{
std::shared_ptr<MockFile> file(new MockFile());
std::shared_ptr<MockFileFactory> factory(new MockFileFactory());
std::shared_ptr<MockFileIterator> internalIterator(new MockFileIterator());
FolderH dummyHandle = {1};
EXPECT_CALL(*internalIterator, getFirst(testing::_,testing::_)).WillOnce(testing::Return(false));
MockFolder::FileIterator iterator = MockFolder::FileIterator(factory,internalIterator,dummyHandle);
EXPECT_CALL(*folder, filesBegin()).WillOnce(testing::Return(iterator));
}
The factory is used to create the item that the iterator is pointing to. This is a mocked version in the case of unit tests. The internal iterator is a wrapper of the functions getFile() and getNextFile(), and all such pairs, with the interface getFirst() and getNext().
I also have functions called loadFileIteratorWithOneItem and loadFileIteratorWithTwoItems.
Can anyone suggest a better way of testing the function b above?
Is my design fundamentally flayed? Is the issue with the iterator implementation?
Upvotes: 1
Views: 922
Reputation: 2323
It seems to me that you're not actually using mocking to it's full potential.
To test b
in this case, I would simply use the following testcase:
TEST (FooTest, b)
{
MockEntry e;
MockFolder f;
IFile::FileIterator it; // I don't know how you construct one,
// just make sure that it == it
ON_CALL(e, root()).WillByDefault(Return(&f));
ON_CALL(f, filesBegin()).WillByDefault(Return(it));
ON_CALL(f, filesEnd()).WillByDefault(Return(it));
EXPECT_CALL(e, root()).Times(1);
EXPECT_CALL(f, filesBegin()).Times(1);
EXPECT_CALL(f, filesEnd()).Times(1);
EXPECT_EQ(b(e), "");
}
This in the most elegant approach using mocks, in my opinion. It is clear what is happening, and you are not depending on any other code to setup the behaviour. This tests just the else-clause of your b
function.
Upvotes: 2