8.3 KiB
Data Generators
Introduced in Catch 2.6.0.
Data generators (also known as data driven/parametrized test cases)
let you reuse the same set of assertions across different input values.
In Catch2, this means that they respect the ordering and nesting
of the TEST_CASE
and SECTION
macros, and their nested sections
are run once per each value in a generator.
This is best explained with an example:
TEST_CASE("Generators") {
auto i = GENERATE(1, 3, 5);
REQUIRE(is_odd(i));
}
The "Generators" TEST_CASE
will be entered 3 times, and the value of
i
will be 1, 3, and 5 in turn. GENERATE
s can also be used multiple
times at the same scope, in which case the result will be a cartesian
product of all elements in the generators. This means that in the snippet
below, the test case will be run 6 (2*3) times.
TEST_CASE("Generators") {
auto i = GENERATE(1, 2);
auto j = GENERATE(3, 4, 5);
}
There are 2 parts to generators in Catch2, the GENERATE
macro together
with the already provided generators, and the IGenerator<T>
interface
that allows users to implement their own generators.
Combining GENERATE
and SECTION
.
GENERATE
can be seen as an implicit SECTION
, that goes from the place
GENERATE
is used, to the end of the scope. This can be used for various
effects. The simplest usage is shown below, where the SECTION
"one"
runs 4 (2*2) times, and SECTION
"two" is run 6 times (2*3).
TEST_CASE("Generators") {
auto i = GENERATE(1, 2);
SECTION("one") {
auto j = GENERATE(-3, -2);
REQUIRE(j < i);
}
SECTION("two") {
auto k = GENERATE(4, 5, 6);
REQUIRE(i != k);
}
}
The specific order of the SECTION
s will be "one", "one", "two", "two",
"two", "one"...
The fact that GENERATE
introduces a virtual SECTION
can also be used
to make a generator replay only some SECTION
s, without having to
explicitly add a SECTION
. As an example, the code below reports 3
assertions, because the "first" section is run once, but the "second"
section is run twice.
TEST_CASE("GENERATE between SECTIONs") {
SECTION("first") { REQUIRE(true); }
auto _ = GENERATE(1, 2);
SECTION("second") { REQUIRE(true); }
}
This can lead to surprisingly complex test flows. As an example, the test below will report 14 assertions:
TEST_CASE("Complex mix of sections and generates") {
auto i = GENERATE(1, 2);
SECTION("A") {
SUCCEED("A");
}
auto j = GENERATE(3, 4);
SECTION("B") {
SUCCEED("B");
}
auto k = GENERATE(5, 6);
SUCCEED();
}
The ability to place
GENERATE
between twoSECTION
s was introduced in Catch 2.13.0.
Provided generators
Catch2's provided generator functionality consists of three parts,
GENERATE
macro, that serves to integrate generator expression with a test case,- 2 fundamental generators
SingleValueGenerator<T>
-- contains only single elementFixedValuesGenerator<T>
-- contains multiple elements
- 5 generic generators that modify other generators
FilterGenerator<T, Predicate>
-- filters out elements from a generator for which the predicate returns "false"TakeGenerator<T>
-- takes firstn
elements from a generatorRepeatGenerator<T>
-- repeats output from a generatorn
timesMapGenerator<T, U, Func>
-- returns the result of applyingFunc
on elements from a different generatorChunkGenerator<T>
-- returns chunks (insidestd::vector
) of n elements from a generator
- 4 specific purpose generators
RandomIntegerGenerator<Integral>
-- generates random Integrals from rangeRandomFloatGenerator<Float>
-- generates random Floats from rangeRangeGenerator<T>
-- generates all values inside an arithmetic rangeIteratorGenerator<T>
-- copies and returns values from an iterator range
ChunkGenerator<T>
,RandomIntegerGenerator<Integral>
,RandomFloatGenerator<Float>
andRangeGenerator<T>
were introduced in Catch 2.7.0.
IteratorGenerator<T>
was introduced in Catch 2.10.0.
The generators also have associated helper functions that infer their type, making their usage much nicer. These are
value(T&&)
forSingleValueGenerator<T>
values(std::initializer_list<T>)
forFixedValuesGenerator<T>
table<Ts...>(std::initializer_list<std::tuple<Ts...>>)
forFixedValuesGenerator<std::tuple<Ts...>>
filter(predicate, GeneratorWrapper<T>&&)
forFilterGenerator<T, Predicate>
take(count, GeneratorWrapper<T>&&)
forTakeGenerator<T>
repeat(repeats, GeneratorWrapper<T>&&)
forRepeatGenerator<T>
map(func, GeneratorWrapper<T>&&)
forMapGenerator<T, U, Func>
(mapU
toT
, deduced fromFunc
)map<T>(func, GeneratorWrapper<U>&&)
forMapGenerator<T, U, Func>
(mapU
toT
)chunk(chunk-size, GeneratorWrapper<T>&&)
forChunkGenerator<T>
random(IntegerOrFloat a, IntegerOrFloat b)
forRandomIntegerGenerator
orRandomFloatGenerator
range(Arithemtic start, Arithmetic end)
forRangeGenerator<Arithmetic>
with a step size of1
range(Arithmetic start, Arithmetic end, Arithmetic step)
forRangeGenerator<Arithmetic>
with a custom step sizefrom_range(InputIterator from, InputIterator to)
forIteratorGenerator<T>
from_range(Container const&)
forIteratorGenerator<T>
chunk()
,random()
and bothrange()
functions were introduced in Catch 2.7.0.
from_range
has been introduced in Catch 2.10.0
range()
for floating point numbers has been introduced in Catch 2.11.0
And can be used as shown in the example below to create a generator that returns 100 odd random number:
TEST_CASE("Generating random ints", "[example][generator]") {
SECTION("Deducing functions") {
auto i = GENERATE(take(100, filter([](int i) { return i % 2 == 1; }, random(-100, 100))));
REQUIRE(i > -100);
REQUIRE(i < 100);
REQUIRE(i % 2 == 1);
}
}
Apart from registering generators with Catch2, the GENERATE
macro has
one more purpose, and that is to provide simple way of generating trivial
generators, as seen in the first example on this page, where we used it
as auto i = GENERATE(1, 2, 3);
. This usage converted each of the three
literals into a single SingleValueGenerator<int>
and then placed them all in
a special generator that concatenates other generators. It can also be
used with other generators as arguments, such as auto i = GENERATE(0, 2, take(100, random(300, 3000)));
. This is useful e.g. if you know that
specific inputs are problematic and want to test them separately/first.
For safety reasons, you cannot use variables inside the GENERATE
macro.
This is done because the generator expression will outlive the outside
scope and thus capturing references is dangerous. If you need to use
variables inside the generator expression, make sure you thought through
the lifetime implications and use GENERATE_COPY
or GENERATE_REF
.
GENERATE_COPY
andGENERATE_REF
were introduced in Catch 2.7.1.
You can also override the inferred type by using as<type>
as the first
argument to the macro. This can be useful when dealing with string literals,
if you want them to come out as std::string
:
TEST_CASE("type conversion", "[generators]") {
auto str = GENERATE(as<std::string>{}, "a", "bb", "ccc");
REQUIRE(str.size() > 0);
}
Generator interface
You can also implement your own generators, by deriving from the
IGenerator<T>
interface:
template<typename T>
struct IGenerator : GeneratorUntypedBase {
// via GeneratorUntypedBase:
// Attempts to move the generator to the next element.
// Returns true if successful (and thus has another element that can be read)
virtual bool next() = 0;
// Precondition:
// The generator is either freshly constructed or the last call to next() returned true
virtual T const& get() const = 0;
};
However, to be able to use your custom generator inside GENERATE
, it
will need to be wrapped inside a GeneratorWrapper<T>
.
GeneratorWrapper<T>
is a value wrapper around a
std::unique_ptr<IGenerator<T>>
.
For full example of implementing your own generator, look into Catch2's examples, specifically Generators: Create your own generator.