I am practicing sorting algorithms in C++ 17 and have implemented my unittests as follows (the following compiles and all tests are green):
template <typename T>
class SortingmethodTest : public ::testing::Test
{
protected:
T sortingmethod;
static constexpr int amount_test_data[7] = {0, 4, 8, 10, 256, 1000, 1234};
};
using sortingmethods = ::testing::Types<STLSort<int>,
InsertionSort<int>,
ShellSort<int>,
MergeSort<int>,
OptimizedMergeSort<int>,
QuickSort<int>>;
TYPED_TEST_SUITE(SortingmethodTest, sortingmethods);
TYPED_TEST(SortingmethodTest, sort)
{
for (const auto& amount : this->amount_test_data)
{
Sortvector<int> test(amount);
test.vul_random_zonder_dubbels(); // Fills the vector
this->sortingmethod(test); // operator() of the sortmethod used (STLSort, InsertionSort, ...) sorts the vector
ASSERT_TRUE(test.is_range());
ASSERT_TRUE(test.is_gesorteerd());
ASSERT_TRUE(std::is_sorted(test.begin(), test.end()));
}
}
TYPED_TEST(SortingmethodTest, sort_reverse)
{
// ...
}
TYPED_TEST(SortingmethodTest, sort_already_sorted)
{
// ...
}
TYPED_TEST(SortingmethodTest, sort_empty)
{
// ...
}
I would like to repeat the same tests for other types than ints, e.g.
STLSort<int>,
InsertionSort<int>,
ShellSort<int>,
MergeSort<int>,
OptimizedMergeSort<int>,
QuickSort<int>
STLSort<double>,
InsertionSort<double>,
ShellSort<double>,
MergeSort<double>,
OptimizedMergeSort<double>,
QuickSort<double>
STLSort<CustomType>,
InsertionSort<CustomType>,
ShellSort<CustomType>,
MergeSort<CustomType>,
OptimizedMergeSort<CustomType>,
QuickSort<CustomType>
...
How can I do this in C++ with google test as cleanly and with as much reuse as possible? I am getting lost in the jungle of typed tests and type-parameterized tests [1]: when should I use one or the other?
With kind regards,
Marten
[1] https://github.com/google/googletest/blob/master/docs/advanced.md#type-parameterized-tests
It is often frustrating that the googletest API so far does not give us more of the leverage of modern C++ to make test code concise, especially for the testing of templates. But up through v1.8.x (the current release series at this date) googletest has been committed to C++98 compatibility, and that is largely why. Forthcoming releases 1.9.x will move on to C++11 compatibility, and we can hope for a more powerful API.
Nevertheless it is possible to write reasonably concise and straightforward googletest code now to do the sort of thing you want: that is, to unit-test congruent templates for varying values of just one template parameter.
There's more than one way of doing it. Here is a worked example of one of them, using type-parameterized tests.
We'll have a set of three templates
template<typename T> struct (AA|BB|CC) {...};
each of which provides (at least) the interface:
Name::Name(T const & u);
Name::operator int() const;
Name Name::operator+(Name const & u) const;
Name & Name::operator+=(Name const & u);
Name Name::operator-(Name const & u) const;
Name & Name::operator-=(Name const & u);
for Name
= (AA|BB|CC)
. We want to
unit-test this interface, for each of (AA|BB|CC)
, each instantiated for
each of the six types:
char, int, float, AA<char>, BB<int>, CC<float>
So that's 18 instantiations to be tested:
AA<char>, AA<int>, AA<float>, AA<AA<char>>, AA<BB<int>>, AA<CC<float>>
BB<char>, BB<int>, BB<float>, BB<AA<char>>, BB<BB<int>>, BB<CC<float>>
CC<char>, CC<int>, CC<float>, CC<AA<char>>, CC<BB<int>>, CC<CC<float>>
To keep it short, we will implement just two generic tests. For objects
a
, b
and c
of any instantiated test type:
a = b + c; b += c
, then a == b
.b != c
, after a = b - c; c -= b
, then a != c
.(At least, these properties should hold as long as the operations don't overflow or lose precision, which I'll just avoid).
So we'll expect to see 36 tests all told.
For this illustration I don't care what AA
, BB
and CC
are, apart from their common interface,
so I'm simply going to derive them identically from a single model, like so:
some_types.h
#pragma once
#include <type_traits>
namespace detail {
template<typename T>
struct bottom_type {
using type = T;
};
template<template<typename ...> class C, typename ...Ts>
struct bottom_type<C<Ts...>> {
using type = typename C<Ts...>::type;
};
}
template<typename T>
using bottom_t = typename detail::bottom_type<T>::type;
template<
typename T,
typename Enable = std::enable_if_t<std::is_arithmetic_v<bottom_t<T>>>
>
struct model
{
using type = bottom_t<T>;
model() = default;
model(model const &) = default;
model(T const & t)
: _t{t}{}
operator type() const { return _t; }
auto operator+(model const & u) const {
return _t + u;
}
auto & operator+=(model const & u) {
_t += u;
return *this;
}
auto operator-(model const & u ) const {
return _t - u;
}
auto & operator-=(model const & u ) {
_t -= u;
return *this;
}
protected:
type _t = 0;
};
template<typename T> struct AA : model<T>{ using model<T>::model; };
template<typename T> struct BB : model<T>{ using model<T>::model; };
template<typename T> struct CC : model<T>{ using model<T>::model; };
Now here's my googletest code:
main.cpp
#include <array>
#include <algorithm>
#include <random>
#include <type_traits>
#include <limits>
#include <gtest/gtest.h>
#include "some_types.h"
template<typename T>
struct fixture : public ::testing::Test
{
protected:
template<typename U>
static auto const & test_data() {
using type = bottom_t<U>;
static std::array<type,1000> data;
static bool called;
if (!called) {
std::default_random_engine gen;
auto low = std::numeric_limits<type>::min() / 2;
auto high = std::numeric_limits<type>::max() / 2;
auto dist = [&low,&high](){
if constexpr (std::is_floating_point_v<type>) {
return std::uniform_real_distribution<type>(low,high);
} else {
return std::uniform_int_distribution<type>(low,high);
}
}();
std::generate(
data.begin(),data.end(),[&dist,&gen](){ return dist(gen); });
called = true;
}
return data;
}
};
template<template<typename> class C, typename ...Ts>
using test_types = ::testing::Types<C<Ts>...>;
using AA_test_types = test_types<AA,char,int,float,AA<char>,BB<int>,CC<float>>;
using BB_test_types = test_types<BB,char,int,float,AA<char>,BB<int>,CC<float>>;
using CC_test_types = test_types<CC,char,int,float,AA<char>,BB<int>,CC<float>>;
TYPED_TEST_SUITE_P(fixture);
TYPED_TEST_P(fixture, addition)
{
using wrapped_type = typename TypeParam::type;
auto const & data = this->template test_data<wrapped_type>();
auto fi = data.begin(); auto ri = data.rbegin();
for ( ; fi != ri.base(); ++fi, ++ri)
{
TypeParam lhs{*fi}, rhs{*ri};
auto sum = lhs + rhs;
lhs += rhs;
ASSERT_EQ(lhs,sum);
}
}
TYPED_TEST_P(fixture, subtraction)
{
using wrapped_type = typename TypeParam::type;
auto const & data = this->template test_data<wrapped_type>();
auto fi = data.begin(); auto ri = data.rbegin();
for ( ; fi != ri.base(); ++fi, ++ri) {
TypeParam lhs{*fi}, rhs{*ri};
if (lhs != rhs) {
auto diff = lhs - rhs;
rhs -= lhs;
ASSERT_NE(rhs,diff);
}
}
}
REGISTER_TYPED_TEST_SUITE_P(fixture,addition,subtraction);
INSTANTIATE_TYPED_TEST_SUITE_P(AA_tests, fixture, AA_test_types);
INSTANTIATE_TYPED_TEST_SUITE_P(BB_tests, fixture, BB_test_types);
INSTANTIATE_TYPED_TEST_SUITE_P(CC_tests, fixture, CC_test_types);
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Let's take in the points of interest:-
template<template<typename> class C, typename ...Ts>
using test_types = ::testing::Types<C<Ts>...>;
Here, I'm making test_types
a template-alias for a ::testing::Types<SomeType...>
list
where SomeType
will be an instantiation of one of the templates under test. As it
happens, my templates AA
, BB
, CC
(like yours) are all of the form:
template<typename T> class;
so I want test_types
to be a:
::testing::Types<C<Ts>...>
Then I define 3 concrete type aliases:
using AA_test_types = test_types<AA,char,int,float,AA<char>,BB<int>,CC<float>>;
using BB_test_types = test_types<BB,char,int,float,AA<char>,BB<int>,CC<float>>;
using CC_test_types = test_types<CC,char,int,float,AA<char>,BB<int>,CC<float>>;
which are respectively equivalent to:
::testing::Types<AA<char>, AA<int>, AA<float>, AA<AA<char>>, AA<BB<int>>, AA<CC<float>>>;
::testing::Types<BB<char>, BB<int>, BB<float>, BB<AA<char>>, BB<BB<int>>, BB<CC<float>>>;
::testing::Types<CC<char>, CC<int>, CC<float>, CC<AA<char>>, CC<BB<int>>, CC<CC<float>>>;
Then I define a type-parameterized test-suite with the template-fixture fixture
.
TYPED_TEST_SUITE_P(fixture);
Then I define my two type-parameterized test-patterns.
TYPED_TEST_P(fixture, addition)
{
using wrapped_type = typename TypeParam::type;
auto const & data = this->template test_data<wrapped_type>();
auto fi = data.begin(); auto ri = data.rbegin();
for ( ; fi != ri.base(); ++fi, ++ri)
{
TypeParam lhs{*fi}, rhs{*ri};
auto sum = lhs + rhs;
lhs += rhs;
ASSERT_EQ(lhs,sum);
}
}
TYPED_TEST_P(fixture, subtraction)
{
using wrapped_type = typename TypeParam::type;
auto const & data = this->template test_data<wrapped_type>();
auto fi = data.begin(); auto ri = data.rbegin();
for ( ; fi != ri.base(); ++fi, ++ri) {
TypeParam lhs{*fi}, rhs{*ri};
if (lhs != rhs) {
auto diff = lhs - rhs;
rhs -= lhs;
ASSERT_NE(rhs,diff);
}
}
}
Then I register both of those patterns for instantiation with each instantiation
of fixture
:
REGISTER_TYPED_TEST_SUITE_P(fixture,addition,subtraction);
Then I create 3 instantiations called (AA|BB|CC)_tests
of fixture
for the
the test type-lists (AA|BB|CC)_test_types
respectively:
INSTANTIATE_TYPED_TEST_SUITE_P(AA_tests, fixture, AA_test_types);
INSTANTIATE_TYPED_TEST_SUITE_P(BB_tests, fixture, BB_test_types);
INSTANTIATE_TYPED_TEST_SUITE_P(CC_tests, fixture, CC_test_types);
And that's it. Compile and link:
$ g++ -std=c++17 -Wall -Wextra -pedantic -o gtester main.cpp -lgtest -pthread
Run:
./gtester
[==========] Running 36 tests from 18 test suites.
[----------] Global test environment set-up.
[----------] 2 tests from AA_tests/fixture/0, where TypeParam = AA<char>
[ RUN ] AA_tests/fixture/0.addition
[ OK ] AA_tests/fixture/0.addition (0 ms)
[ RUN ] AA_tests/fixture/0.subtraction
[ OK ] AA_tests/fixture/0.subtraction (1 ms)
[----------] 2 tests from AA_tests/fixture/0 (1 ms total)
[----------] 2 tests from AA_tests/fixture/1, where TypeParam = AA<int>
[ RUN ] AA_tests/fixture/1.addition
[ OK ] AA_tests/fixture/1.addition (0 ms)
[ RUN ] AA_tests/fixture/1.subtraction
[ OK ] AA_tests/fixture/1.subtraction (0 ms)
[----------] 2 tests from AA_tests/fixture/1 (0 ms total)
...
...
...
[----------] 2 tests from CC_tests/fixture/4, where TypeParam = CC<BB<int> >
[ RUN ] CC_tests/fixture/4.addition
[ OK ] CC_tests/fixture/4.addition (0 ms)
[ RUN ] CC_tests/fixture/4.subtraction
[ OK ] CC_tests/fixture/4.subtraction (0 ms)
[----------] 2 tests from CC_tests/fixture/4 (0 ms total)
[----------] 2 tests from CC_tests/fixture/5, where TypeParam = CC<CC<float> >
[ RUN ] CC_tests/fixture/5.addition
[ OK ] CC_tests/fixture/5.addition (0 ms)
[ RUN ] CC_tests/fixture/5.subtraction
[ OK ] CC_tests/fixture/5.subtraction (0 ms)
[----------] 2 tests from CC_tests/fixture/5 (0 ms total)
[----------] Global test environment tear-down
[==========] 36 tests from 18 test suites ran. (4 ms total)
[ PASSED ] 36 tests.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With