Sitelet https://github.com/hegeldev/hegel-java
Skip to content

Repository files navigation

Important

We're excited you're checking out Hegel! Hegel is in beta, and we'd love for you to try it and report any feedback.

As part of our beta, we may make breaking changes if it makes Hegel a better property-based testing library. If that instability bothers you, please check back in a few months for a stable release!

See https://hegel.dev/compatibility for more details.

Hegel for Java

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

Instead of writing tests with hand-picked example inputs, you describe a property that should hold for all inputs and let Hegel generate inputs to try to falsify it. When it finds a failing input it automatically shrinks it to a minimal counterexample.

Installation

Hegel for Java ships as two interchangeable artifacts with the same API — pick the one that matches your JVM:

  • dev.hegel:hegel — requires Java 22+; binds the engine over the Foreign Function & Memory API with no extra dependencies.
  • dev.hegel:hegel-jna — requires Java 17+; binds the engine over JNA.

Add the dependency with Maven:

<dependency>
  <groupId>dev.hegel</groupId>
  <artifactId>hegel</artifactId> <!-- or "hegel-jna" for Java 17-21 -->
  <version>0.1.0</version>
  <scope>test</scope>
</dependency>

or with Gradle:

testImplementation("dev.hegel:hegel:0.1.0") // or "dev.hegel:hegel-jna:0.1.0"

Depend on exactly one of the two — they contain the same classes and differ only in how they call the native engine. The engine is bundled in both jars for Linux (x86-64 and arm64), macOS (Apple Silicon), and Windows (x86-64 and arm64). Both pull in a third, small artifact, dev.hegel:hegel-lowlevel, which holds the binding contract; you never need to depend on it directly unless you are binding Hegel yourself.

Because Hegel calls native code, pass --enable-native-access=ALL-UNNAMED to silence the JVM's native-access warning — printed by JDK 22+ for hegel (FFM) and by JDK 24+ for hegel-jna (JNA, under JEP 472). The flag is accepted on every supported JDK (17+), so it is safe to set unconditionally. With Maven Surefire:

<argLine>--enable-native-access=ALL-UNNAMED</argLine>

Quickstart

Here's a quick example of how to write a Hegel test:

import static dev.hegel.Generators.integers;
import static dev.hegel.Generators.lists;
import static org.junit.jupiter.api.Assertions.assertEquals;

import dev.hegel.HegelTest;
import dev.hegel.TestCase;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;

class SortTest {
  static List<Integer> mySort(List<Integer> xs) {
    return new ArrayList<>(new TreeSet<>(xs)); // oops: a TreeSet removes duplicates
  }

  @HegelTest
  void sortingPreservesLength(TestCase tc) {
    List<Integer> xs = tc.draw(lists(integers()), "xs");
    assertEquals(xs.size(), mySort(xs).size());
  }
}

This test will fail when run with mvn test! Hegel will produce a minimal failing test case for us:

xs = [0, 0];

org.opentest4j.AssertionFailedError: expected: <2> but was: <1>

Hegel reports the minimal example showing that our sort is incorrectly dropping duplicates: [0, 0], two equal elements, which mySort collapses into one. If we replace the TreeSet-based body of mySort() with a sort that keeps duplicates, this test will then pass.

The optional "xs" label passed to draw names the value in the falsifying-example output. See the API documentation for a full tour of generators, combinators, control functions, and settings.

Stateful testing

Model-based tests drive a state machine whose @Rule methods are the actions and whose @Invariant methods are the properties, with the engine choosing and shrinking the action sequence:

Stateful.run(new IntegerStack(), tc);

Run the same machine with a concurrency bound to look for races: the engine draws how many worker threads apply the rules at once, @Rule(group = "...") says which rules may overlap, and a ConcurrentPool passes generated values between concurrent rules.

Stateful.run(new Counter(tc), tc, Stateful.options().maxConcurrency(4));

See the Stateful Javadoc for complete sequential and concurrent examples. The concurrent one, a counter that loses updates under contention, is checked in as a runnable demonstration that is skipped unless asked for; run it from a checkout with

mvn test -pl hegel -am -Dtest=ConcurrentCounterExample -Dhegel.examples=true -Dsurefire.failIfNoSpecifiedTests=false

(or -pl hegel-jna -am for the JNA frontend) and it fails with a report of the rounds and the worker-stamped rule and draw lines that led to the lost update.

Using Hegel as a library

Frontends for other JVM languages (or custom runners) use Hegel.run instead of Hegel.test. It returns a RunReport rather than throwing, and a Reporter lets you own every line of output:

RunReport report = Hegel.run(tc -> { ... }, new Settings().testCases(200), Reporter.silent());
report.status();                        // PASSED, FAILED, or ERROR
report.statistics();                    // valid / invalid / overrun / interesting case counts
for (Failure f : report.failures()) {   // one per distinct counterexample
  f.draws();                            // labelled draws of the minimal example, as Java values
  f.exception();                        // the body's own throwable
  f.reproduceBlob();                    // Optional: replay it later with Settings.reproduceFailure
  f.caveat();                           // Optional: how reliably a nondeterministic failure reproduced
}

TestCase.isFinal() identifies the executions the engine stamped for the failure report (the final replay of a counterexample among them), TestCase.span and Label let custom composite generators tell the engine about their structure, and Settings.infrastructurePackages keeps a frontend's own stack frames out of failure origins.

Binding Hegel yourself

dev.hegel:hegel-lowlevel (Java 17+, no dependencies) is the binding contract on its own, for two audiences that do not want the Java frontend:

  • Writing a binding — implement dev.hegel.lowlevel.Libhegel (one method per hegel_* function in hegel.h, with raw handles and return codes) over your FFI mechanism, and register it as a dev.hegel.lowlevel.LibhegelBackend service provider. The two bundled bindings are registered the same way.
  • Building a frontend from scratch — depend on hegel-lowlevel plus one binding, call Libhegel.load() to get the engine, and drive the run loop and per-case primitives yourself. Abi holds the constants; LibraryLoader resolves the shared library.

The package is experimental: new engine functions become new Libhegel methods.

About

Property-based testing for Java, built on Hypothesis

Resources

Stars

25 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages