Google Test - generate values for template class instanciation

余生长醉 提交于 2021-01-28 00:14:09

问题


I am having an issue with Google Test. I have a code equivalent to what follows:


A.h

template<int N> class A {
public:
    A() {}
    A(int n) {
        var = n;
    }
    int var;
};

test_A.cpp

#include "gtest/gtest.h"
#include "A.h"

template<typename T> class TestA : public ::testing::Test {};

TYPED_TEST_CASE_P(TestA);

TYPED_TEST_P(TestA, SomeTest){
    TypeParam x(0);
    EXPECT_EQ(x.var,0);
    ...
}

REGISTER_TYPED_TEST_CASE_P(TestA, SomeTest);

typedef ::testing::Types<A<0>, A<1>, A<2>, A<3>, A<4>, A<5> ... A<511>, A<512>> MyTypes;
INSTANTIATE_TYPED_TEST_CASE_P(My, TestA, MyTypes);

int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

To sum up, I want to run the test SomeTeston class A for many versions of N. With the type-parameterized test developed above, the problem is that with "typed test" method, I have to write manually all versions of class A I want to test.

With Google Test, you can also write value-parameterized tests, which is very convenient because you can use generators like Range:

INSTANTIATE_TEST_CASE_P(InstantiationName, TestA, ::testing::Range(0,512,1));

and then, you can access the values using the function GetParam() inside the test. This syntax would make what I want to do very easy to setup. However, it seems that the values are not resolved at compile time, and they are therefore not usable to specify templates values.

My questions are:

  • Is there some syntax which is equivalent to value-parameterized testing in Google Test to specify all versions of A I want to test?
  • If there is not, how can I generate my list of types A<0>, ..., A<512> to be used with type-parameterized testing?

N.B: It has to be c++11 compliant.


回答1:


You're looking for a way to economise fingerwork in the sort of googletest solution you've sketched above by avoiding the hardcoding of the typelist:

A<0>, A<1>, A<2>, A<3>, A<4>, A<5>, ... A<511>, A<512>

in the definition of MyTypes, and having it somehow generated at compiletime given just the length of the typelist.

Regrettably, there's a very limited amount of fingerwork you can save by solving this problem. Internally, googletest employs C++03 exclusively. So no variadic templates, which arrived with C++11. Hence there's a hardcoded limit on the length of the typelist in:

::testing::Types<T0, T1, ... Tn>

You're wanting a list of 513 types: the hardcoded limit is 50. After that compilation fails.

Still, if you're going to be doing a lot with this type of googletest solution, with typelist lengths in the N x 10 range, it might be worth your while having code in stock to generate them the way you want.

C++14 has just the tool for jobs like this. It is template< class T, T... Ints> class std::integer_sequence. But you say that you're restricted to C++11. In that case, you'll need a hand-rolled substitute for std::integer_sequence. There are lots out in there in Google land. Jonathan Wakeley's one stands out: std::integer_sequence was his C++14 proposal, and his implementation is the prototype for the C++14 standard one.

For now I'll make do with my own weedy one. Here's a header file:

integer_seq.h

#ifndef INTEGER_SEQ_H
#define INTEGER_SEQ_H

#if __cplusplus >= 201402L

#include <utility>

template<typename IntType, IntType ...Is>
using integer_seq = std::integer_sequence<IntType,Is...>;

template<typename IntType, IntType N>
using make_integer_seq = std::make_integer_sequence<IntType,N>;

#elif __cplusplus == 201103L

#include <type_traits>

template<typename IntType, IntType ...Is> struct integer_seq {};

namespace detail {

template<typename IntType, IntType Count, bool Done, IntType ...Is>
struct gen_seq;

template<typename IntType, IntType Count, IntType ...Is>
struct gen_seq<IntType, Count, false, Is...>
{
    static_assert(Count > 0,"Count must be positive");    
    using type = 
        typename gen_seq<   IntType, 
                            Count - 1, Count - 1 == 0, 
                            Count - 1, Is...
                        >::type;
};

template <typename IntType, IntType Count, IntType ...Is>
struct gen_seq<IntType, Count, true, Is...>
{
    using type = integer_seq<IntType,Is...>;
};

} // namespace detail


template<typename IntType, IntType N>
using make_integer_seq = typename detail::gen_seq<IntType,N,N == 0>::type;

#else
#error At least C++11 required :(
#endif

#endif

This header defines:

template< typename IntType, IntType ...Is> struct integer_seq;

which represents a sequence Is... of values of some integral type IntType, and:

template<typename IntType, IntType N> make_integer_seq;

which is an alias for the type:

integer_sequence<IntType,0,... N - 1>

If you compile in C++14 then integer_seq is itself just an alias for std::integer_sqeuence. If C++11 then integer_seq is hand-rolled.

Once you've got template integer_seq, you can use it to get some leverage on googletest's template ::testing::Types. Here's another header file:

indexed_type_list

#ifndef INDEXED_TYPE_LIST_H
#define INDEXED_TYPE_LIST_H

#include <cstddef>
#include "gtest/gtest.h"
#include "integer_seq.h"

template<typename IntType, template<IntType> class T, typename Seq>
struct indexed_type_list;

template<typename IntType, template<IntType> class T, IntType ...Is>
struct indexed_type_list<IntType,T,integer_seq<IntType,Is...>>
{
    using type = ::testing::Types<T<Is>...>;
};

template<typename IntType, template<IntType> class T,  std::size_t Size>
using indexed_type_list_t =
    typename indexed_type_list<IntType,T,make_integer_seq<IntType,Size>>::type;

#endif

which defines:

template<typename IntType, template<IntType> class T,  IntType Size>
indexed_type_list_t;

such that, given some integral type IntType, a number Size of that type, and some unary non-type template template<IntType> class T, then:

indexed_type_list_t<IntType,T,Size>

is an alias for:

::testing::Types<T<0>,... T<Size - 1>>;

which is what you're after. indexed_type_list is admittedly a very lame name for this concept, but my imagaination fails me.

Now we ready to roll. Here's how we'd adapt your specimen solution:

main.cpp

#include "indexed_type_list.h"
#include "A.h"

template<typename T> class TestA : public ::testing::Test{};

TYPED_TEST_CASE_P(TestA);

TYPED_TEST_P(TestA, SomeTest){
    TypeParam x(0);
    EXPECT_EQ(x.var,0);
}

REGISTER_TYPED_TEST_CASE_P(TestA, SomeTest);

using MyTypes = indexed_type_list_t<int,A,50>;

INSTANTIATE_TYPED_TEST_CASE_P(My, TestA, MyTypes);

int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

Compile and link:

$ g++ -Wall -Wextra -pedantic -std=c++11 -c main.cpp
$ g++ -o testrunner main.o -lgtest -pthread

And it runs like:

$ ./testrunner
[==========] Running 50 tests from 50 test cases.
[----------] Global test environment set-up.
[----------] 1 test from My/TestA/0, where TypeParam = A<0>
[ RUN      ] My/TestA/0.SomeTest
[       OK ] My/TestA/0.SomeTest (0 ms)
[----------] 1 test from My/TestA/0 (0 ms total)

[----------] 1 test from My/TestA/1, where TypeParam = A<1>
[ RUN      ] My/TestA/1.SomeTest
[       OK ] My/TestA/1.SomeTest (0 ms)
[----------] 1 test from My/TestA/1 (0 ms total)

...
...

[----------] 1 test from My/TestA/48, where TypeParam = A<48>
[ RUN      ] My/TestA/48.SomeTest
[       OK ] My/TestA/48.SomeTest (0 ms)
[----------] 1 test from My/TestA/48 (0 ms total)

[----------] 1 test from My/TestA/49, where TypeParam = A<49>
[ RUN      ] My/TestA/49.SomeTest
[       OK ] My/TestA/49.SomeTest (0 ms)
[----------] 1 test from My/TestA/49 (0 ms total)

[----------] Global test environment tear-down
[==========] 50 tests from 50 test cases ran. (0 ms total)
[  PASSED  ] 50 tests.

So that's your problem solved. It's all optional for here on.

The flip side

There's a fatal design flaw in this approach to testing any class template over a range of non-type template parameters. It was there before we solved the problem of generating the typelist for ::testing::Types, and it's still there.

In real applications, proficiently designed, you don't come across a class template like the template<int N> class A defined in A.h, where the template parameter N is not used at all, either in the template definition or for template specializations. That template parameter has no practical purpose. The definition of A might as well be:

class A {
public:
    A() {}
    A(int n) {
        var = n;
    }
    int var;
};

Suppose that instead of A we had to deal with a marginally more lifelike template:

dot_matrix.h

#ifndef DOT_MATRIX_H
#define DOT_MATRIX_H

#include <array>

template<std::size_t N>
struct dot_matrix
{
    bool get(std::size_t x, std::size_t y) const {
        return _square.at(x).at(y);
    }
    void set(std::size_t x, std::size_t y) {
        _square.at(x).at(y) = true;
        ++_filled;
    }
    void clear(std::size_t x, std::size_t y){
        _square.at(x).at(y) = false;
        --_filled;
    }

    unsigned filled() const {
        return _filled;
    }

    unsigned vacant() const {
        return area() - _filled;
    }

    static constexpr std::size_t side() {
        return N * 2;
    }

    static constexpr std::size_t area() {
        return side() * side();
    }

private:
    unsigned _filled;
    std::array<std::array<bool,N>,N> _square;
};

#endif

Supposedly,template<std::size_t N> struct dot_matrix generically encapsulates a fixed-size square dot matrix of size N x N. I say supposedly because right now it's let down by a bug - true, an unrealistically obvious bug - that we'd hope to flush out by googletest unit-testing.

You draw the straw to code the unit-testing and get everything setup up for TYPED_TEST_CASE_P style testing, with:

using MyTypes = ::testing::Types<dot_matrix<0>,... dot_matrix<49>>;

It doesn't matter whether you compiletime-generate

dot_matrix<0>,... dot_matrix<49>

or code it hard way.

You write a bunch of TYPED_TEST_Ps, for instance:

TYPED_TEST_P(TestDotMatrix,IsSquare){
    TypeParam matrix;
    auto side = matrix.side();
    EXPECT_EQ(matrix.area(),side * side));
}

TYPED_TEST_P(TestDotMatrix, IsConsistent){
    TypeParam matrix;
    auto cap = matrix.filled() + matrix.vacant();
    EXPECT_EQ(matrix.area(),cap));
}
...

The team leader code reviews and spots:-

  • A bug in dot_matrix<N>. It doesn't encapsulate an N x N dot matrix. Actually, it encapsulates a 2N x 2N dot matrix.

  • You haven't got a test that verifies that dot_matrix<N> contains an N x N matrix.

You get back on the case to fix this oversight:-

TYPED_TEST_P(TestDotMatrix, SizeIsRight){
    TypeParam matrix;
    EXPECT_EQ(???,matrix.side());
}

But you can't. You cannot fill in ???, because TypeParam is just a dot_matrix<N> for some value of N that the test does not know, and that N is what needs replace ???.

Because none of the tests know the value of N with which their TypeParam was instantiated, they can all test only behaviours of dot_matrix<N> that are invariant with respect to N, just like that:

TYPED_TEST_P(TestA, SomeTest){
    TypeParam x(0);
    EXPECT_EQ(x.var,0);
}

above. Testing a behaviour that doesn't vary with N for 50 different values of N isn't useful. It is useful to have tests for those N-invariant behaviours, of course. Just not for 50 different values of N, or indeed, more than one.

The behaviours that do vary with N also need to be tested. Like: Does a dot_matrix<N> report the right size and area for that N?

It is for these tests that your googletest solution requires a range that varies N in instantiations of the TYPED_TEST_Ps. And here is what needs to be be appreciated to get those TYPED_TEST_Ps out the '???' quandry:-

The fact that a template struct dot_matrix<std::size_t N> is under test is invariant. None of the tests needs to be given this information: the test range doesn't need to convey it. The only thing that varies is N. The value of N is the one thing that each test needs to know. The range that you require to instantiate the tests is a range of values of N.

That presents a difficulty, at first sight, for a googletest solution.

Googletest offers a framework for generating Value Parameterized Tests that can instantiate a TEST_P for each of a range of values given as runtime arguments to ::testing::Values(...).

Googletest also offers frameworks for generating Typed Tests and Type Parameterized Tests that can instantiate a TYPED_TEST or TYPED_TEST_P for each of a range of types given as compiletime template arguments to ::testing::Types<...>.

But we need a range of values of a non-type template parameter. C++ requires such a parameter to be a compiletime integral constant. The googletest options for test ranges do not include a range of compiletime integral constants.

Happily, C++11 or later comes to the rescue with a way to map an integral constant uniquely to a type. It is template< class IntType, IntType v > struct std::integral_constant. For any constant N of integral type IntType:

struct std::integral_constant<IntType,N>;

is the uniquely mapped type, and from that type you can retrieve a compiletime constant N as:

std::integral_constant<IntType,N>::value;

So here in bare essense is the googletest solution:

...
using MyTypes =
    ::testing::Types<std::integral_constant<0>,...std::integral_constant<49>>;

...
...

TYPED_TEST_P(TestDotMatrix, SizeIsRight){
    dot_matrix<TypeParam::value> matrix;
    EXPECT_EQ(TypeParam::value,matrix.side());
}

and here's a fully working example, with the testing::Types range compiletime-generated as before.

A new header:

integral_constant_typelist.h

#ifndef INTEGRAL_CONSTANT_TYPELIST_H
#define INTEGRAL_CONSTANT_TYPELIST_H

#include <cstddef>
#include <type_traits>
#include "gtest/gtest.h"
#include "integer_seq.h"

template<typename IntType, IntType Start,typename Seq>
struct integral_constant_typelist;

template<typename IntType, IntType Start,IntType ...Is>
struct integral_constant_typelist<IntType,Start,integer_seq<IntType,Is...>>
{
    using type =
        ::testing::Types<std::integral_constant<IntType,Start + Is>...>;
};

template<typename IntType, IntType Start,std::size_t Size>
using integral_constant_typelist_t =
    typename integral_constant_typelist<
        IntType,Start,make_integer_seq<IntType,Size>
    >::type;

#endif

This header, unlike indexed_type_list.h, is not badly named. It's a candidate for your googletest toolkit. It defines:

template<typename IntType, IntType Start,std::size_t Size>
integral_constant_typelist_t

to be the type of:

::testing::Types<std::integral_constant<IntType,Start>,...
                    std::integral_constant<IntType,Start + (Size - 1)>

So e.g.

integral_constant_typelist_t<int,3,10>

is the type of:

::testing::Types<
    std::integral_constant<int,3>,...std::integral_constant<int,12>>

Then here's a test suite for dot_matrix<N>:

main.cpp

#include "integral_constant_typelist.h"
#include "dot_matrix.h"

using arbitrary = std::integral_constant<std::size_t,42>;

// A fixture template for N-variant tests
template<
    typename T // = `std::integral_constant<std::size_t, N>`, for some `N`
> struct TestDotMatrixVariant : ::testing::Test
{
    using int_type = typename T::value_type; // = std::size_t
    static constexpr int_type N_param() {
        return T::value;    // = N
    }
    using test_type = dot_matrix<N_param()>;
    dot_matrix<N_param()> const & get_specimen() const {
        return _specimen;
    }
    dot_matrix<N_param()> & get_specimen() {
        return _specimen;
    }
protected:
    test_type _specimen;
};

// A fixture for invariant tests
struct TestDotMatrixInvariant : TestDotMatrixVariant<arbitrary>{};

// Invariant test
TEST_F(TestDotMatrixInvariant,IsSquare){
    auto const & specimen = get_specimen();
    auto side = specimen.side();
    EXPECT_EQ(specimen.area(),side * side);
}

// Another invariant test
TEST_F(TestDotMatrixInvariant, IsConsistent){
    auto const & specimen = get_specimen();
    auto cap = specimen.filled() + specimen.vacant();
    EXPECT_EQ(specimen.area(),cap);
}

// Yet another invariant test
TEST_F(TestDotMatrixInvariant,OutOfRangeGetXThrows)
{
    auto const & specimen = get_specimen();
    auto x = specimen.side() + 1;
    EXPECT_THROW(specimen.get(x,0),std::out_of_range);
}

// More invariant tests...

// An N-variant test case
TYPED_TEST_CASE_P(TestDotMatrixVariant);

// An N-variant test
TYPED_TEST_P(TestDotMatrixVariant,SizeIsRight){
    EXPECT_EQ(this->N_param(),this->get_specimen().side());
}

REGISTER_TYPED_TEST_CASE_P(TestDotMatrixVariant,SizeIsRight);

using dot_matrices_0_50 = integral_constant_typelist_t<std::size_t,0,50>;

INSTANTIATE_TYPED_TEST_CASE_P(N_0_to_50,TestDotMatrixVariant,dot_matrices_0_50);

// More N-variant test cases and N-variant tests...

int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

Compile and link:

$ g++ -Wall -Wextra -pedantic -std=c++11 -c main.cpp
$ g++ -o testrunner main.o -lgtest -pthread

Run:

$ ./testrunner
[==========] Running 53 tests from 51 test cases.
[----------] Global test environment set-up.
[----------] 3 tests from TestDotMatrixInvariant
[ RUN      ] TestDotMatrixInvariant.IsSquare
[       OK ] TestDotMatrixInvariant.IsSquare (0 ms)
[ RUN      ] TestDotMatrixInvariant.IsConsistent
[       OK ] TestDotMatrixInvariant.IsConsistent (0 ms)
[ RUN      ] TestDotMatrixInvariant.OutOfRangeGetXThrows
[       OK ] TestDotMatrixInvariant.OutOfRangeGetXThrows (0 ms)
[----------] 3 tests from TestDotMatrixInvariant (0 ms total)

[----------] 1 test from N_0_to_50/TestDotMatrixVariant/0, where TypeParam = std::integral_constant<unsigned long, 0ul>
[ RUN      ] N_0_to_50/TestDotMatrixVariant/0.SizeIsRight
[       OK ] N_0_to_50/TestDotMatrixVariant/0.SizeIsRight (0 ms)
[----------] 1 test from N_0_to_50/TestDotMatrixVariant/0 (0 ms total)

[----------] 1 test from N_0_to_50/TestDotMatrixVariant/1, where TypeParam = std::integral_constant<unsigned long, 1ul>
[ RUN      ] N_0_to_50/TestDotMatrixVariant/1.SizeIsRight
main.cpp:58: Failure
Expected equality of these values:
  this->N_param()
    Which is: 1
  this->get_specimen().side()
    Which is: 2
[  FAILED  ] N_0_to_50/TestDotMatrixVariant/1.SizeIsRight, where TypeParam = std::integral_constant<unsigned long, 1ul> (0 ms)
[----------] 1 test from N_0_to_50/TestDotMatrixVariant/1 (0 ms total)
...
...
...
[----------] 1 test from N_0_to_50/TestDotMatrixVariant/49, where TypeParam = std::integral_constant<unsigned long, 49ul>
[ RUN      ] N_0_to_50/TestDotMatrixVariant/49.SizeIsRight
main.cpp:58: Failure
Expected equality of these values:
  this->N_param()
    Which is: 49
  this->get_specimen().side()
    Which is: 98
[  FAILED  ] N_0_to_50/TestDotMatrixVariant/49.SizeIsRight, where TypeParam = std::integral_constant<unsigned long, 49ul> (0 ms)
[----------] 1 test from N_0_to_50/TestDotMatrixVariant/49 (0 ms total)

[----------] Global test environment tear-down
[==========] 53 tests from 51 test cases ran. (1 ms total)
[  PASSED  ] 4 tests.
[  FAILED  ] 49 tests, listed below:
[  FAILED  ] N_0_to_50/TestDotMatrixVariant/1.SizeIsRight, where TypeParam = std::integral_constant<unsigned long, 1ul>
...
...
...
[  FAILED  ] N_0_to_50/TestDotMatrixVariant/49.SizeIsRight, where TypeParam = std::integral_constant<unsigned long, 49ul>

49 FAILED TESTS

All of the invariant tests passed. Variant test SizeIsRight passed, as it should, for dot_matrix<0>. Then it failed, as it should, for dot_matrix<1> through dot_matrix<49>.

Better debug:

static constexpr std::size_t side() {
    // return N * 2; <-- Wrong
    return N; // <-- Right
}

Then:

$ ./testrunner
[==========] Running 53 tests from 51 test cases.
[----------] Global test environment set-up.
[----------] 3 tests from TestDotMatrixInvariant
[ RUN      ] TestDotMatrixInvariant.IsSquare
[       OK ] TestDotMatrixInvariant.IsSquare (0 ms)
[ RUN      ] TestDotMatrixInvariant.IsConsistent
[       OK ] TestDotMatrixInvariant.IsConsistent (0 ms)
[ RUN      ] TestDotMatrixInvariant.OutOfRangeGetXThrows
[       OK ] TestDotMatrixInvariant.OutOfRangeGetXThrows (0 ms)
[----------] 3 tests from TestDotMatrixInvariant (0 ms total)

[----------] 1 test from N_0_to_50/TestDotMatrixVariant/0, where TypeParam = std::integral_constant<unsigned long, 0ul>
[ RUN      ] N_0_to_50/TestDotMatrixVariant/0.SizeIsRight
[       OK ] N_0_to_50/TestDotMatrixVariant/0.SizeIsRight (0 ms)
[----------] 1 test from N_0_to_50/TestDotMatrixVariant/0 (0 ms total)
...
...
...
[----------] 1 test from N_0_to_50/TestDotMatrixVariant/49, where TypeParam = std::integral_constant<unsigned long, 49ul>
[ RUN      ] N_0_to_50/TestDotMatrixVariant/49.SizeIsRight
[       OK ] N_0_to_50/TestDotMatrixVariant/49.SizeIsRight (0 ms)
[----------] 1 test from N_0_to_50/TestDotMatrixVariant/49 (0 ms total)

[----------] Global test environment tear-down
[==========] 53 tests from 51 test cases ran. (1 ms total)
[  PASSED  ] 53 tests.

All good.



来源:https://stackoverflow.com/questions/48890291/google-test-generate-values-for-template-class-instanciation

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!