Expand description
Hegel is a property-based testing library for Rust. 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
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);
}To change the number of test cases at runtime — for the whole suite,
without editing source — set the HEGEL_TEST_CASES environment variable:
HEGEL_TEST_CASES=10000 cargo testWhen set and non-empty, it replaces the value the profile would give
every test that does not set test_cases itself; an explicit
test_cases attribute is compiled in and keeps its value.
To see what a test actually generates, record events with
TestCase::event and TestCase::event_value and enable the
end-of-run statistics report with the HEGEL_STATISTICS environment
variable (or Settings::show_statistics):
HEGEL_STATISTICS=1 cargo test my_test -- --nocaptureThree more variables cover the settings a single cargo test invocation
most often needs to change: HEGEL_SEED fixes the seed (or clears one
a profile set with none), HEGEL_DERANDOMIZE turns the derived
per-test seed on or off, and HEGEL_PRINT_BLOB turns the
#[hegel::reproduce_failure("…")] line on or off, so a failure can be
captured for replay without editing the test:
HEGEL_SEED=7 HEGEL_PRINT_BLOB=true cargo test my_testThe variables are read by the engine when a Settings value is
created, so they win over profiles and hegel.toml but not over
settings written into the test itself.
§Settings profiles
Suite-wide settings live in named profiles. Hegel ships three:
development (what local runs get), ci (selected automatically on CI
servers), and workload (selected automatically inside
Antithesis). Modify them or define your own
in a hegel.toml at your package or workspace root:
[profiles.development]
test_cases = 200
[profiles.nightly]
test_cases = 10000A profile layers over whichever profile the environment selects, so on
CI nightly resolves as nightly → ci and locally as nightly →
development, unless it pins a parent with extends. Select one for a
test with
#[hegel::test(profile = "nightly")], or as the suite-wide default with
a default = "nightly" entry at the top of hegel.toml or the
HEGEL_DEFAULT_PROFILE environment variable:
HEGEL_DEFAULT_PROFILE=nightly cargo testThe docs::settings page covers the whole settings system: every
setting, the layers it can be set in and how they combine, what each
shipped profile sets, the reserved base and default names, profile
inheritance, every hegel.toml key, the HEGEL_CONFIG variable for
naming the config file directly, and the programmatic
Settings::register_profile and Settings::set_default_profile.
§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 test case’s outcome — the whole family passes, fails, or is rejected as one test case — but each clone draws from its own independent, deterministic stream of choices, so several threads can generate concurrently without perturbing each other’s values and the same seed replays the same values on every stream.
In a failing example’s report, a clone’s drawn values appear together, at the point where the clone was created — not interleaved by wall-clock timing — so the report is deterministic no matter how the threads were scheduled.
Determinism extends only as far as your own code’s determinism: if your
threads race on shared state, Hegel replays each stream faithfully but
the test may still behave differently run to run — see TestCase’s
documentation for the full contract and the patterns that are safe to
rely on.
§The engine library
Hegel’s engine is libhegel_c, a shared library that hegeltest’s build
script compiles and your tests load at runtime, so the engine’s Rust
dependencies never appear in your cargo graph. cargo test and
cargo run find the library automatically. A binary that runs anywhere
else — a deployed #[hegel::main] fuzzer, say — needs the library
shipped next to the executable, its directory named in the
HEGEL_C_LIB_DIR environment variable, or a copy installed where the
platform’s own library search looks (LD_LIBRARY_PATH and friends),
which is tried last. HEGEL_C_LIB_DIR overrides the whole search,
including in the build script, where a prebuilt library lets offline
builds skip the compile. Whichever copy is found must be the exact
engine version this crate was built against. A mismatched library is
refused on load with an error naming both versions.
Alternatively, the static-engine feature links the engine into your
binary as an ordinary Rust dependency. Binaries are then self-contained,
but the engine’s entire dependency tree becomes visible to your build,
where it is subject to cargo feature unification and can even change
type inference in unrelated code (a PartialEq<serde_json::Value> impl
is enough to make assert_eq!(true, ...) ambiguous). Opt in where
self-contained binaries matter more than that isolation.
§Learning more
- Browse the
generatorsmodule for the full list of available generators. use hegel::prelude::*;brings in the imports these examples spell out (seeprelude).- Read the
prettymodule docs for how a failing test reports the values it drew, and how to make your own types printable. - See
Settingsfor more configuration settings to customise how your test runs.
Re-exports§
pub use generators::Generator;pub use generators::PrintableGenerator;pub use pretty::Document;pub use pretty::PrettyPrinter;pub use runner::Backend;pub use runner::HealthCheck;pub use runner::NondeterminismStrictness;pub use runner::Phase;pub use runner::ProfileError;pub use runner::Settings;pub use runner::Verbosity;
Modules§
- docs
- Guides to the parts of Hegel that span several items. Each module here is documentation only and defines nothing.
- extras
- Integrations with third-party libraries.
- generators
- Generators for producing test data.
- prelude
- The imports nearly every hegel test wants in scope.
- pretty
- Pretty-printing of generated values: how a failing test reports what it drew, and how to take part in that report.
- 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 defined in another crate.
- one_of
- Choose from any number of generators of the same type.
- tuples
- Creates a tuple generator from 0–12 component generators.
Structs§
- Explicit
Test Case - A test case with pre-defined values for explicit/example-based testing.
- Profile
Error - Why
Settings::try_from_profilecould not resolve a profile: the name is unknown, or ahegel.tomlis malformed. TheDisplayimpl carries the engine’s diagnostic. - Settings
- Configuration for a Hegel test run.
- Test
Case - A handle to the current test case.
Enums§
- Backend
- Selects the source of randomness the engine draws from.
- Health
Check - Health checks that can be suppressed during test execution.
- Nondeterminism
Strictness - How a run reacts when it detects nondeterministic test behavior — a test whose structure or outcome changes when the same choices are replayed.
- Phase
- Controls which phases of the test lifecycle are executed.
- Verbosity
- Controls how much output Hegel produces during test runs.
Attribute Macros§
- composite
- Define a composite generator from a function.
- concurrent_
state_ machine - Derive a
ConcurrentStateMachineimplementation from animplblock, for concurrent stateful testing viastateful::Machine::run_concurrent. - 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.
- reproduce_
failure - Replay a failing example from a base64 failure blob.
- standalone_
function - Rewrite a function taking a
TestCaseplus additional arguments into one that takes just those arguments and internally runs Hegel. - state_
machine - Derive a
StateMachineimplementation from animplblock. - test
- The main entrypoint into Hegel.
- test_
helper - Name the draws in a helper function after their bindings.
Derive Macros§
- Default
Generator - Derive a generator for a struct or enum.