Hegel 0.3.5
Property-based testing for C++
Loading...
Searching...
No Matches
Hegel

Hegel is a property-based testing library for C++. Hegel is based on Hypothesis, using the Hegel protocol.

Getting started

This guide walks you through the basics of installing Hegel and writing your first tests.

Install Hegel

Using CMake:

include(FetchContent)
FetchContent_Declare(
hegel
GIT_REPOSITORY https://github.com/hegeldev/hegel-cpp.git
GIT_TAG v0.3.5
)
FetchContent_MakeAvailable(hegel)
target_link_libraries(your_target PRIVATE hegel)

Hegel requires C++20, CMake 3.14, and uv on the PATH.

Write your first test

You're now ready to write your first test. In a new file:

#include <hegel/hegel.h>
namespace gs = hegel::generators;
int main() {
int n = tc.draw(gs::integers<int>());
if (n != n) { // integers should always be equal to themselves
throw std::runtime_error("self-equality failed");
}
});
return 0;
}
Handle to the currently-executing test case.
Definition test_case.h:34
T draw(const generators::Generator< T > &gen) const
Draw a random value from a generator.
Definition core.h:371
Hegel generators.
Definition core.h:16
void test(const std::function< void(TestCase &)> &test_fn, const Settings &settings={})
Run a Hegel test.

Now build and run the test. You should see that this test passes.

Let's look at what's happening in more detail. hegel::test() runs your test callback many times (100, by default). The callback receives a TestCase, which provides a TestCase::draw() method for drawing different values. This test draws a random integer and checks that it should be equal to itself.

Next, try a test that fails:

int n = tc.draw(gs::integers<int>());
if (n >= 50) { // this will fail!
throw std::runtime_error("n should be below 50");
}
});

This test asserts that any integer is less than 50, which is obviously incorrect. Hegel will find a test case that makes this assertion fail, and then shrink it to find the smallest counterexample — in this case, n = 50.

To fix this test, you can constrain the integers you generate with the min_value and max_value parameters:

int n = tc.draw(gs::integers<int>({.min_value = 0, .max_value = 49}));
if (n >= 50) {
throw std::runtime_error("n should be below 50");
}
});

Run the test again. It should now pass.

Use generators

Hegel provides a rich library of generators in the hegel::generators namespace that you can use out of the box. There are primitive generators, such as integers, floats, and text, and combinators that allow you to make generators out of other generators, such as vectors and tuples.

For example, you can use vectors to generate a vector of integers:

namespace gs = hegel::generators;
auto vector = tc.draw(gs::vectors(gs::integers<int>()));
auto initial_length = vector.size();
vector.push_back(tc.draw(gs::integers<int>()));
if (vector.size() <= initial_length) {
throw std::runtime_error("push_back should increase size");
}
});

This test checks that appending an element to a random vector of integers should always increase its length.

You can also define custom generators. For example, say you have a Person struct that we want to generate:

struct Person {
int age;
std::string name;
};
auto generate_person() {
return gs::compose([](const hegel::TestCase& tc) {
int age = tc.draw(gs::integers<int>());
std::string name = tc.draw(gs::text());
return Person{age, name};
});
}

Note that you can feed the results of a draw to subsequent calls. For example, say that you extend the Person struct to include a driving_license boolean field:

struct Person {
int age;
std::string name;
bool driving_license;
};
auto generate_person() {
return gs::compose([](const hegel::TestCase& tc) {
int age = tc.draw(gs::integers<int>());
std::string name = tc.draw(gs::text());
bool driving_license =
age >= 18 ? tc.draw(gs::booleans()) : false;
return Person{age, name, driving_license};
});
}

Hegel can also derive generators automatically for reflectable structs via default_generator. This uses reflect-cpp to inspect the struct's fields and pick an appropriate generator for each:

struct Person {
std::string name;
int age;
};
Person p = tc.draw(gs::default_generator<Person>());
});

Call .override(...) on the returned generator to customize individual fields (see override).

Debug your failing test cases

Use TestCase::note to attach debug information:

int x = tc.draw(gs::integers<int>());
int y = tc.draw(gs::integers<int>());
tc.note("x + y = " + std::to_string(x + y) +
", y + x = " + std::to_string(y + x));
if (x + y != y + x) {
throw std::runtime_error("addition is not commutative");
}
});
void note(std::string_view message) const
Record a message that will be printed on the final replay of a failing test case.

Notes only appear when Hegel replays the minimal failing example.

Change the number of test cases

By default Hegel runs 100 test cases. To override this, pass a Settings struct as the second argument to hegel::test():

int n = tc.draw(gs::integers<int>());
if (n != n) {
throw std::runtime_error("self-equality failed");
}
}, {.test_cases = 500});

Learning more

  • Browse the hegel::generators namespace for the full list of available generators.
  • See Settings for more configuration settings to customise how your test runs.