Update documentation and examples for generators

This commit is contained in:
Martin Hořeňovský 2019-01-29 10:52:28 +01:00
parent 5929d9530c
commit 061f1f836a
No known key found for this signature in database
GPG Key ID: DE48307B8B0D381A
5 changed files with 241 additions and 30 deletions

View File

@ -1,50 +1,125 @@
<a id="top"></a> <a id="top"></a>
# Data Generators # Data Generators
_Generators are currently considered an experimental feature and their
API can change between versions freely._
Data generators (also known as _data driven/parametrized test cases_) Data generators (also known as _data driven/parametrized test cases_)
let you reuse the same set of assertions across different input values. let you reuse the same set of assertions across different input values.
In Catch2, this means that they respect the ordering and nesting In Catch2, this means that they respect the ordering and nesting
of the `TEST_CASE` and `SECTION` macros. of the `TEST_CASE` and `SECTION` macros, and their nested sections
are run once per each value in a generator.
How does combining generators and test cases work might be better
explained by an example:
This is best explained with an example:
```cpp ```cpp
TEST_CASE("Generators") { TEST_CASE("Generators") {
auto i = GENERATE( range(1, 11) ); auto i = GENERATE(1, 2, 3);
SECTION("one") {
SECTION( "Some section" ) { auto j = GENERATE( -3, -2, -1 );
auto j = GENERATE( range( 11, 21 ) ); REQUIRE(j < i);
REQUIRE(i < j);
} }
} }
``` ```
the assertion will be checked 100 times, because there are 10 possible The assertion in this test case will be run 9 times, because there
values for `i` (1, 2, ..., 10) and for each of them, there are 10 possible are 3 possible values for `i` (1, 2, and 3) and there are 3 possible
values for `j` (11, 12, ..., 20). values for `j` (-3, -2, and -1).
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.
## 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
* `ValueGenerator<T>` -- contains only single element
* `ValuesGenerator<T>` -- contains multiple elements
* 4 generic generators that modify other generators
* `FilterGenerator<T, Predicate>` -- filters out elements from a generator
for which the predicate returns "false"
* `TakeGenerator<T>` -- takes first `n` elements from a generator
* `RepeatGenerator<T>` -- repeats output from a generator `n` times
* `MapGenerator<T, U, Func>` -- returns the result of applying `Func`
on elements from a different generator
The generators also have associated helper functions that infer their
type, making their usage much nicer. These are
* `value(T&&)` for `ValueGenerator<T>`
* `values(std::initializer_list<T>)` for `ValuesGenerator<T>`
* `filter(predicate, GeneratorWrapper<T>&&)` for `FilterGenerator<T, Predicate>`
* `take(count, GeneratorWrapper<T>&&)` for `TakeGenerator<T>`
* `repeat(repeats, GeneratorWrapper<T>&&)` for `RepeatGenerator<T>`
* `map(func, GeneratorWrapper<T>&&)` for `MapGenerator<T, T, Func>` (map `T` to `T`)
* `map<T>(func, GeneratorWrapper<U>&&)` for `MapGenerator<T, U, Func>` (map `U` to `T`)
And can be used as shown in the example below to create a generator
that returns 100 odd random number:
You can also combine multiple generators by concatenation:
```cpp ```cpp
static int square(int x) { return x * x; } TEST_CASE("Generating random ints", "[example][generator]") {
TEST_CASE("Generators 2") { SECTION("Deducing functions") {
auto i = GENERATE(0, 1, -1, range(-20, -10), range(10, 20)); auto i = GENERATE(take(100, filter([](int i) { return i % 2 == 1; }, random(-100, 100))));
CAPTURE(i); REQUIRE(i > -100);
REQUIRE(square(i) >= 0); REQUIRE(i < 100);
REQUIRE(i % 2 == 1);
}
} }
``` ```
This will call `square` with arguments `0`, `1`, `-1`, `-20`, ..., `-11`, _Note that `random` is currently not a part of the first-party generators_.
`10`, ..., `19`.
----------
Because of the experimental nature of the current Generator implementation, Apart from registering generators with Catch2, the `GENERATE` macro has
we won't list all of the first-party generators in Catch2. Instead you one more purpose, and that is to provide simple way of generating trivial
should look at our current usage tests in generators, as seen in the first example on this page, where we used it
[projects/SelfTest/UsageTests/Generators.tests.cpp](/projects/SelfTest/UsageTests/Generators.tests.cpp). as `auto i = GENERATE(1, 2, 3);`. This usage converted each of the three
For implementing your own generators, you can look at their implementation in literals into a single `ValueGenerator<int>` and then placed them all in
[include/internal/catch_generators.hpp](/include/internal/catch_generators.hpp). 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.**
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`:
```cpp
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:
```cpp
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](../examples/300-Gen-OwnGenerator.cpp).

View File

@ -14,6 +14,9 @@
- Report: [TeamCity reporter](../examples/207-Rpt-TeamCityReporter.cpp) - Report: [TeamCity reporter](../examples/207-Rpt-TeamCityReporter.cpp)
- Listener: [Listeners](../examples/210-Evt-EventListeners.cpp) - Listener: [Listeners](../examples/210-Evt-EventListeners.cpp)
- Configuration: [Provide your own output streams](../examples/231-Cfg-OutputStreams.cpp) - Configuration: [Provide your own output streams](../examples/231-Cfg-OutputStreams.cpp)
- Generators: [Create your own generator](../examples/300-Gen-OwnGenerator.cpp)
- Generators: [Use variables in generator expressions](../examples/310-Gen-VariablesInGenerators.cpp)
## Planned ## Planned

View File

@ -0,0 +1,59 @@
// 300-Gen-OwnGenerator.cpp
// Shows how to define a custom generator.
// Specifically we will implement a random number generator for integers
// It will have infinite capacity and settable lower/upper bound
#include <catch2/catch.hpp>
#include <random>
// This class shows how to implement a simple generator for Catch tests
class RandomIntGenerator : public Catch::Generators::IGenerator<int> {
std::minstd_rand m_rand;
std::uniform_int_distribution<> m_dist;
int current_number;
public:
RandomIntGenerator(int low, int high):
m_rand(std::random_device{}()),
m_dist(low, high)
{
static_cast<void>(next());
}
int const& get() const override;
bool next() override {
current_number = m_dist(m_rand);
return true;
}
};
// Avoids -Wweak-vtables
int const& RandomIntGenerator::get() const {
return current_number;
}
// This helper function provides a nicer UX when instantiating the generator
// Notice that it returns an instance of GeneratorWrapper<int>, which
// is a value-wrapper around std::unique_ptr<IGenerator<int>>.
Catch::Generators::GeneratorWrapper<int> random(int low, int high) {
return Catch::Generators::GeneratorWrapper<int>(std::unique_ptr<Catch::Generators::IGenerator<int>>(new RandomIntGenerator(low, high)));
}
// The two sections in this test case are equivalent, but the first one
// is much more readable/nicer to use
TEST_CASE("Generating random ints", "[example][generator]") {
SECTION("Nice UX") {
auto i = GENERATE(take(100, random(-100, 100)));
REQUIRE(i >= -100);
REQUIRE(i <= 100);
}
SECTION("Creating the random generator directly") {
auto i = GENERATE(take(100, GeneratorWrapper<int>(std::unique_ptr<IGenerator<int>>(new RandomIntGenerator(-100, 100)))));
REQUIRE(i >= -100);
REQUIRE(i <= 100);
}
}
// Compiling and running this file will result in 400 successful assertions

View File

@ -0,0 +1,72 @@
// 310-Gen-VariablesInGenerator.cpp
// Shows how to use variables when creating generators.
// Note that using variables inside generators is dangerous and should
// be done only if you know what you are doing, because the generators
// _WILL_ outlive the variables -- thus they should be either captured
// by value directly, or copied by the generators during construction.
#include <catch2/catch.hpp>
#include <random>
// Lets start by implementing a parametrizable double generator
class RandomDoubleGenerator : public Catch::Generators::IGenerator<double> {
std::minstd_rand m_rand;
std::uniform_real_distribution<> m_dist;
double current_number;
public:
RandomDoubleGenerator(double low, double high):
m_rand(std::random_device{}()),
m_dist(low, high)
{
static_cast<void>(next());
}
double const& get() const override;
bool next() override {
current_number = m_dist(m_rand);
return true;
}
};
// Avoids -Wweak-vtables
double const& RandomDoubleGenerator::get() const {
return current_number;
}
// Also provide a nice shortcut for creating the generator
Catch::Generators::GeneratorWrapper<double> random(double low, double high) {
return Catch::Generators::GeneratorWrapper<double>(std::unique_ptr<Catch::Generators::IGenerator<double>>(new RandomDoubleGenerator(low, high)));
}
TEST_CASE("Generate random doubles across different ranges",
"[generator][example][advanced]") {
// Workaround for old libstdc++
using record = std::tuple<double, double>;
// Set up 3 ranges to generate numbers from
auto r = GENERATE(table<double, double>({
record{3, 4},
record{-4, -3},
record{10, 1000}
}));
// This will not compile (intentionally), because it accesses a variable
// auto number = GENERATE(take(50, random(r.first, r.second)));
// We have to manually register the generators instead
// Notice that we are using value capture in the lambda, to avoid lifetime issues
auto number = Catch::Generators::generate( CATCH_INTERNAL_LINEINFO,
[=]{
using namespace Catch::Generators;
return makeGenerators(take(50, random(std::get<0>(r), std::get<1>(r))));
}
);
REQUIRE(std::abs(number) > 0);
}
// Compiling and running this file will result in 150 successful assertions

View File

@ -44,6 +44,8 @@ set( SOURCES_IDIOMATIC_TESTS
110-Fix-ClassFixture.cpp 110-Fix-ClassFixture.cpp
120-Bdd-ScenarioGivenWhenThen.cpp 120-Bdd-ScenarioGivenWhenThen.cpp
210-Evt-EventListeners.cpp 210-Evt-EventListeners.cpp
300-Gen-OwnGenerator.cpp
310-Gen-VariablesInGenerators.cpp
) )
# main-s for reporter-specific test sources: # main-s for reporter-specific test sources: