How Hegel works
At the highest level, Hegel defines a protocol for communication between a server and a client.
- The server implements the core of property-based testing. Data generation, shrinking, and so on.
- The client implements the user-facing syntax of properties and generators. It asks the server for generated data via protocol requests.
As an example, suppose we have the following hegel-rust test (the details of the code under test are unimportant):
use hegel::{TestCase}use hegel::generators::{integers}
#[hegel::test(test_cases = 200)]fn test_a(tc: TestCase) { let n: i32 = tc.draw(integers().min_value(100))}When this test runs:
- If this is the first Hegel test to run in the test suite,
hegel-rustspawns the server as a subprocess and negotiates an initial handshake with it. This negotiated connection and subprocess is then reused for any further tests.- The handshake is for example used to communicate the server version to
hegel-rust.
- The handshake is for example used to communicate the server version to
hegel-rusttells the server a new test is being run, and with what settings. Here, the settings aretest_cases=200.hegel-rusttells the server a new test case is being started.hegel-rustexecutestest_afor that test case. Whentc.drawis called,hegel-rustsends the schema representing the generator to the server. The server generates an arbitrary matching value and returns it.- Here, the schema is simply
{"type": "integers", "min_value": 100}. The server might return anything in the range[100, inf].
- Here, the schema is simply
- The test case finishes.
hegel-rustcommunicates this to the server. - After 200 test cases, the test finishes.
hegel-rustcommunicates this to the server. - If the test fails,
hegel-rustcommunicates this to the server. The server then shrinks the failing test case and returns the minimal failing test case tohegel-rust, who displays it to the user.
The transport layer of the protocol is currently unix sockets, but the protocol is agnostic to the particular choice of transport layer and this could in principle be swapped for something else.
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 the server. And the server needs the ability to communicate errors to the client, for example in the case of a flaky test or an invalid generator definition.