How Hegel works
At the highest level, Hegel splits a property-based testing library into two parts:
libhegelimplements the core of property-based testing, including data generation, shrinking, the example database, and so on. It is written in Rust.- The library implements the user-facing syntax of properties and generators for a particular language. It asks
libhegelfor generated data as your test runs.
As an example, suppose we have the following hegel-rust test:
use hegel::{TestCase}use hegel::generators::{integers}
#[hegel::test(test_cases = 100)]fn test_a(tc: TestCase) { let n: i32 = tc.draw(integers().min_value(100))}When this test runs, hegel-rust:
- Builds a settings handle describing how the test should run. It then calls
hegel_run_startto create a run. - Calls
hegel_next_test_caseto asklibhegelfor a test case. - Executes
test_a. Whentc.drawis called,hegel-rustcalls the typed draw primitive for the generator. Here, that ishegel_generate_integerwithmin_value = 100.libhegelreturns some value in the range[100, i32::MAX]. - The test case finishes.
hegel-rustreports the outcome withhegel_mark_complete. It is valid if the body ran to completion, interesting if the property failed, invalid if the test case was rejected, or overran if too much data was generated. - If no failure is found after 100 valid test cases,
hegel_next_test_casereturnsNULLand the test finishes. - If the test finds a failure,
hegel-rustcommunicates this tolibhegel, andlibhegelshrinks the failing test case.hegel-rustreplays the failure and displays it to the user.
We have glossed over some subtlety here. For example, tc.assume() and generator.filter() can reject test cases during the test, which needs to be communicated back to libhegel. And libhegel needs the ability to communicate errors to hegel-rust, for example in the case of a flaky test or an invalid generator definition.
For the full details, see the libhegel reference.