[C++] Typed Tests For Interfaces With Googletest
Find this example here on GitHub.
It's always pretty nice to have encapsulated code where interfaces are used properly. And even better: When the unittests as clean as the according code. Therefore I'm writing about typed tests with Googletest.
Some information upfront about this article:
- We are creating an interface for a book store to read books from a configuration file (similar to the example from my last article)
- Assume we know all derived classes
- All derived classes shall run the same test suite
- This article is about setting up the test environment for the interface
- For better reading I simplified the code snippets in this article
Example Test Case
We are using XML and JSON configuration files to add books to a book store. Therefore we are creating two configuration readers: xml_book_reader
and json_book_reader
. Both inhereting from book_reader
.
To keep this example simple:
- To add books
void add_books()
needs to be implemented - All books are stored in a map
books
in the base classbook_reader
book_reader
has implemented public getters to access added books
The interface and the book data struct
For this example I defined a Book
with the following members and below the book_reader
interface.
struct Book {
std::string author;
std::string title;
int published;
};
class book_reader {
protected:
std::unordered_map<std::size_t, Book> books;
public:
virtual ~book_reader() = default;
virtual void add_books() = 0;
Book get_book(const std::size_t id) const;
const auto get_books() const noexcept;
};
Derived xml_book_reader
and json_book_reader
Now we have the derived classes xml_book_reader
and json_book_reader
. Note that in each constructor we pass the according xml/json file. Both of them implement the add_books()
method to parse the desired configuration file.
// xml_book_reader.hpp
class xml_book_reader : public book_reader {
private:
boost::property_tree::ptree pt;
public:
xml_book_reader(const std::string& file);
void add_books() override;
};
// json_book_reader.hpp
class json_book_reader : public book_reader {
private:
boost::property_tree::ptree pt;
public:
json_book_reader(const std::string& file);
void add_books() override;
};
Test Data
We have one xml configuration and one json configuration. Both contain the same books. These two files will be read in our unittest.
<!-- example.xml -->
<books>
<book>
<id>123456</id>
<author>Buzz Michelangelo</author>
<title>The Story Of Buzz Michelangelo</title>
<published>2019</published>
</book>
<book>
<id>456789</id>
<author>Moxie Crimefighter</author>
<title>How To Fight Crimes</title>
<published>2005</published>
</book>
</books>
// example.json
{
"books": [
{
"id": "123456",
"author": "Buzz Michelangelo",
"title": "The Story Of Buzz Michelangelo",
"published": "2019"
},
{
"id": "456789",
"author": "Moxie Crimefighter",
"title": "How To Fight Crimes",
"published": "2005"
}
]
}
In addition we need sort of ground truth data, to verify if the xml/json parsing works correct. We create the namespace book_tests
, where we have the same two books as test_book
in a container expected_books
namespace book_tests {
const std::string xml_file{"/absolute/path/to/example.xml"};
const std::string json_file{"/absolute/path/to/example.json"};
struct test_book {
std::size_t id;
Book book;
};
const std::map<std::size_t, Book> expected_books = {
{123456, Book{"Buzz Michelangelo","The Story Of Buzz Michelangelo",2019}},
{456789, Book{"Moxie Crimefighter","How To Fight Crimes",2005}}
};
}
This file is created with the a template by CMake's configure_file(..). With that we can run the unittests independently from the working directory and machine. CMake will take care of this file.
Setting Up The Test Suite
First of all we need functions to create concrete book_readers
. Note that the xml_book_reader
receives the xml file and the json_book_reader
the json file in it's constructor.
template <class T>
std::unique_ptr<book_reader> create_book_reader();
template <>
std::unique_ptr<book_reader> create_book_reader<xml_book_reader>() {
return std::make_unique<xml_book_reader>(book_tests::xml_file);
}
template <>
std::unique_ptr<book_reader> create_book_reader<json_book_reader>() {
return std::make_unique<json_book_reader>(book_tests::json_file);
}
Then we continue with the test fixture class. This class inherits from testing::Test
to overwrite the SetUp()
and TearDown()
method. The templated type will be xml_book_reader
and json_book_reader
.
In the constructor we call the just created function create_book_reader
with respect to the templated type. Class member books
is then set to xml_reader
or json_reader
.
template <class T>
class book_reader_tests : public testing::Test {
protected:
book_reader_tests() : books(create_book_reader<T>()) {}
void SetUp() override {
books->add_books();
}
void TearDown() override {
books.reset();
}
std::unique_ptr<book_reader> books;
};
We are almoste done setting up the test suite, we need to list all the types we want to test, by creating a type definition which is passed to Googletest:
typedef Types<xml_book_reader, json_book_reader> Implementations;
TYPED_TEST_SUITE(book_reader_tests, Implementations);
Writing The Actual Unittest
Now we are done setting up the test suite. All tests we'll write will performed on the xml_reader
and the json_reader
.
The test signature has to be: TYPED_TEST(TestCaseName, TestName)
.
To illustrate this example I added one test to the test suite:
- Add all books from the configuration
- Loop over all books which shall be added (from book_tests::expected_books)
- Get the book from the book_container
- Verify if the book is the expected one
I printed some output to the terminal to visualize the Book
content.
TYPED_TEST(book_reader_tests, find_all_books) {
const auto books_container = this->books->get_books();
for (const auto& expected : book_tests::expected_books) {
const auto found = books_container.find(expected.first);
std::cout << "expected book: " << expected.second.author << '\n';
std::cout << "found book: " << found->second.author << "\n\n";
EXPECT_EQ(expected.first, found->first);
EXPECT_EQ(expected.second, found->second);
}
}
And now sit back and relax. We can run all tests with all classes which inherit from book_reader
.
Conclusion
From my point of view there is no discussion about it: When you have interfaces, test the interfaces and run the same tests for all derived classes. Also in the long run, when other concrete implementations are needed or some old ones are replaced, you always can use the same test suite. For some more information about the typed test, check out googletests repository.
Find this example here on GitHub.
That's it for now.
Best Thomas