Skip to main content

Crate hegel

Crate hegel 

Source
Expand description

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

§Getting started with Hegel for Rust

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

§Prerequisites

You will need uv installed and on your PATH.

§Install Hegel

Add hegel-rust to your Cargo.toml as a dev dependency using cargo:

cargo add --dev hegeltest

§Write your first test

You’re now ready to write your first test. We’ll use Cargo as a test runner for the purposes of this guide. Create a new test in the project’s tests/ directory:

use hegel::TestCase;
use hegel::generators as gs;

#[hegel::test]
fn test_integer_self_equality(tc: TestCase) {
    let n = tc.draw(gs::integers::<i32>());
    assert_eq!(n, n); // integers should always be equal to themselves
}

Now run the test using cargo test --test <filename>. You should see that this test passes.

Let’s look at what’s happening in more detail. The #[hegel::test] attribute runs your test many times (100, by default). The test function (in this case test_integer_self_equality) takes a TestCase parameter, which provides a 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:

#[hegel::test]
fn test_integers_always_below_50(tc: TestCase) {
    let n = tc.draw(gs::integers::<i32>());
    assert!(n < 50); // this will fail!
}

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 functions:

#[hegel::test]
fn test_bounded_integers_always_below_50(tc: TestCase) {
    let n = tc.draw(gs::integers::<i32>()
        .min_value(0)
        .max_value(49));
    assert!(n < 50);
}

Run the test again. It should now pass.

§Use generators

Hegel provides a rich library of generators 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 vecs and tuples.

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

use hegel::generators as gs;

#[hegel::test]
fn test_append_increases_length(tc: TestCase) {
    let mut vector = tc.draw(gs::vecs(gs::integers::<i32>()));
    let initial_length = vector.len();
    vector.push(tc.draw(gs::integers::<i32>()));
    assert!(vector.len() > initial_length);
}

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:

#[derive(Debug)]
struct Person {
    age: i32,
    name: String,
}

#[hegel::composite]
fn generate_person(tc: TestCase) -> Person {
    let age = tc.draw(gs::integers::<i32>());
    let name = tc.draw(gs::text());
    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:

#[derive(Debug)]
struct Person {
    age: i32,
    name: String,
    driving_license: bool,
}

#[hegel::composite]
fn generate_person(tc: TestCase) -> Person {
    let age = tc.draw(gs::integers::<i32>());
    let name = tc.draw(gs::text());
    let driving_license = if age >= 18 {
        tc.draw(gs::booleans())
    } else {
         false
    };
    Person { age, name, driving_license }
}

§Debug your failing test cases

Use the note method to attach debug information:

#[hegel::test]
fn test_with_notes(tc: TestCase) {
    let x = tc.draw(gs::integers::<i32>());
    let y = tc.draw(gs::integers::<i32>());
    tc.note(&format!("x + y = {}, y + x = {}", x + y, y + x));
    assert_eq!(x + y, y + x);
}

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 the test_cases argument to the test attribute:

#[hegel::test(test_cases = 500)]
fn test_integers_many(tc: TestCase) {
    let n = tc.draw(gs::integers::<i32>());
    assert_eq!(n, n);
}

§Threading

TestCase is Send but not Sync: you can clone it and move the clone to another thread to drive generation from there.

use hegel::TestCase;
use hegel::generators as gs;

#[hegel::test]
fn test_with_worker_thread(tc: TestCase) {
    let tc_worker = tc.clone();
    let handle = std::thread::spawn(move || {
        tc_worker.draw(gs::vecs(gs::integers::<i32>()).max_size(10))
    });
    let xs = handle.join().unwrap();
    let more: bool = tc.draw(gs::booleans());
    let _ = (xs, more);
}

Clones share the same backend connection — they are views onto one test case, not independent test cases. Individual backend calls are serialised by a shared mutex, so code like “spawn worker, worker draws, join, main thread draws” is deterministic.

Using threads is currently extremely fragile and should only be used with extreme caution right now. You are liable to get flaky test failures when multiple threads draw concurrently. We intend to support this use case increasingly well over time, but right now it is a significant footgun — see TestCase’s documentation for the full contract and the patterns that are safe to rely on.

§Learning more

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

Re-exports§

pub use explicit_test_case::ExplicitTestCase;
pub use generators::Generator;

Modules§

backend
explicit_test_case
generators
Generators for producing test data.
stateful
Stateful (model-based) testing support.

Macros§

compose
Create a generator from imperative code that draws from other generators.
derive_generator
Derive a generator for a struct type defined externally.
one_of
Choose from multiple generators of the same type.
tuples
Creates a tuple generator from 0–12 component generators.

Structs§

Settings
Configuration for a Hegel test run.
TestCase
A handle to the current test case.

Enums§

HealthCheck
Health checks that can be suppressed during test execution.
Mode
Controls the test execution mode.
Verbosity
Controls how much output Hegel produces during test runs.

Attribute Macros§

composite
Define a composite generator from a function.
explicit_test_case
Define an explicit test case to run before the property-based test.
main
Turn a function into a standalone Hegel binary entry point.
standalone_function
Rewrite a function taking a TestCase plus additional arguments into one that takes just those arguments and internally runs Hegel.
state_machine
Derive a StateMachine implementation from an impl block.
test
The main entrypoint into Hegel.

Derive Macros§

DefaultGenerator
Derive a generator for a struct or enum.