hegeltest_flutter — Property-based testing for Flutter
Flutter integration for hegeltest — property-based testing powered by Hegel's native fuzzing engine.
Why this package?
hegeltest depends on package:test. Flutter projects use flutter_test. This package bridges the gap by providing hegelFlutterTest() which uses flutter_test's test() function while giving you full access to all hegeltest generators.
Quick Start
dev_dependencies:
hegeltest_flutter: ^0.8.0
flutter_test:
sdk: flutter
import 'package:flutter_test/flutter_test.dart';
import 'package:hegeltest_flutter/hegeltest_flutter.dart';
void main() {
hegelFlutterTest('reverse is involutory', (tc) {
final xs = tc.draw(lists(integers()));
expect(xs.reversed.toList().reversed.toList(), equals(xs));
});
}
Run with:
flutter test
API
hegelFlutterTest() accepts all the same parameters as hegelTest():
testCases— number of random inputs to try (default: 100)seed— fixed seed for reproducibilityreproduce— replay a specific failure blobdatabase— whether to persist and replay counterexamples from disk (default:true; explicit parameter overridesHEGEL_DATABASE)databasePath— custom path to store counterexamples (default:.hegel/examples)databaseKey— identifier for the test in the database (defaults to the testdescription)config—HegelConfigfor reusable settingssetUpEach/tearDownEach— per-iteration lifecycle hooks- All
flutter_testparameters:timeout,tags,skip,retry
All generators from package:hegeltest are re-exported:
- Primitives:
integers(),doubles(),booleans(),bigIntegers() - Text:
text(),fromRegex(),emails(),urls(),uuids() - Collections:
lists(),sets(),maps() - Combinators:
oneOf(),sampled(),nullable(),tuples2/3/4() - Temporal:
dates(),times(),dateTimes() - Network:
ipv4Addresses(),ipv6Addresses() - Bytes:
bytes()
Stateful Testing
For complex, state-dependent systems, hegeltest_flutter supports stateful property-based testing. This allows you to generate random sequences of operations and verify that your system invariants hold at every step:
import 'package:flutter_test/flutter_test.dart';
import 'package:hegeltest_flutter/hegeltest_flutter.dart';
class Counter {
int value = 0;
void increment(int step) => value += step;
void decrement(int step) => value -= step;
void reset() => value = 0;
}
class CounterMachine extends StateMachine {
final counter = Counter();
int model = 0;
@override
List<StateRule> get rules => [
StateRule('increment', execute: (tc) {
final step = tc.draw(integers(min: 1, max: 10));
counter.increment(step);
model += step;
}),
StateRule('decrement', execute: (tc) {
final step = tc.draw(integers(min: 1, max: 5));
counter.decrement(step);
model -= step;
}),
StateRule('reset', execute: (tc) {
counter.reset();
model = 0;
}),
];
@override
List<StateInvariant> get invariants => [
StateInvariant(
'counter value matches model',
check: (tc) {
expect(counter.value, equals(model));
},
),
];
}
void main() {
hegelFlutterStatefulTest('counter works', () => CounterMachine());
}
Widget Testing
hegeltest_flutter includes hegelFlutterWidgetTest which wraps testWidgets(), allowing you to run property-based tests on your Flutter UI. The callback receives both a TestCase (for drawing random values) and a WidgetTester (for pumping widgets).
Basic example: generating random text and verifying it renders without errors.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hegeltest_flutter/hegeltest_flutter.dart';
void main() {
hegelFlutterWidgetTest('text widget renders correctly', (tc, tester) async {
final label = tc.draw(text(minSize: 1, maxSize: 50));
await tester.pumpWidget(MaterialApp(home: Scaffold(body: Text(label))));
expect(find.text(label), findsOneWidget);
});
}
Config sweep example: generate random widget configs, pump, and verify no overflow.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hegeltest_flutter/hegeltest_flutter.dart';
void main() {
hegelFlutterWidgetTest('padding does not cause overflow', (tc, tester) async {
final left = tc.draw(integers(min: 0, max: 100)).toDouble();
final top = tc.draw(integers(min: 0, max: 100)).toDouble();
final right = tc.draw(integers(min: 0, max: 100)).toDouble();
final bottom = tc.draw(integers(min: 0, max: 100)).toDouble();
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Padding(
padding: EdgeInsets.fromLTRB(left, top, right, bottom),
child: const SizedBox(width: 50, height: 50),
),
),
),
);
expect(find.byType(SizedBox), findsOneWidget);
});
}
Accessibility Monkey Fuzzing
hegelFlutterMonkeyTest traverses Flutter's active accessibility (SemanticsOwner) tree, discovers interactive nodes (buttons, text fields, scrollables, sliders), and fuzzes action sequences (tap, longPress, scroll, setText, increase/decrease, dismiss).
When an unhandled crash or assertion failure occurs, Hegel's native Rust engine automatically shrinks the action sequence to the minimal steps to reproduce and formats a step trace:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hegeltest_flutter/hegeltest_flutter.dart';
void main() {
hegelFlutterMonkeyTest(
'fuzzes checkout workflow without crashing',
createWidget: (tc) => const CheckoutApp(),
steps: 25,
allowedActions: [
SemanticsAction.tap,
SemanticsAction.setText,
SemanticsAction.scrollDown,
],
invariant: (tc, tester) async {
// Invariant checked after every sequence
expect(find.byType(CheckoutApp), findsOneWidget);
},
);
}
If a crash occurs, Hegel reports the exact minimal steps:
Monkey fuzzing caught an error after 3 step(s):
1. tap on "Add Coupon" (id=14)
2. setText on "Coupon Code" (id=18) with "DISCOUNT99"
3. tap on "Apply" (id=19)
Cause: RangeError (index): Invalid value: Valid value range is empty: 0
Layout & Screen Size Invariant Sweeps
Dynamic accessibility font sizes (TextScaler), compact screens, and right-to-left (RTL) localizations are common sources of layout crashes in Flutter (A RenderFlex overflowed by ... pixels).
hegelFlutterLayoutSweepTest automatically sweeps viewport dimensions, screen densities, font scaling factors, and text directions:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hegeltest_flutter/hegeltest_flutter.dart';
void main() {
hegelFlutterLayoutSweepTest(
'user profile card never overflows across screens and text scales',
sweepConfig: const LayoutSweepConfig(
minWidth: 320,
maxWidth: 1024,
minHeight: 480,
maxHeight: 1200,
minTextScale: 0.8,
maxTextScale: 2.5,
),
builder: (tc, sample) => const UserProfileCard(),
);
}
When an overflow occurs, Hegel shrinks the viewport and text scale parameters to find the exact minimal boundary condition causing the bug:
Layout invariant violated:
Configuration: LayoutSample(320.0x480.0, textScale: 2.10x, direction: rtl, dpr: 1.0, keyboard: down, brightness: light)
Error: A RenderFlex overflowed by 14 pixels on the right.
Standalone Runner
For custom testing tools, CI scripts, or programmatic analysis, use runHegelFlutterTest(). It returns a RunResult without wrapping inside flutter_test:
final result = await runHegelFlutterTest((tc) {
final a = tc.draw(integers());
final b = tc.draw(integers());
assert(a + b == b + a);
});
print(result.status); // RunStatus.passed
print(result.testCasesRun); // 100
Collecting Statistics
Use tc.collect() to inspect the distribution of generated values across your test runs:
hegelFlutterTest('string reverse is involutory', (tc) {
final s = tc.draw(text());
tc.collect(
s.isEmpty ? 'empty' : (s.length < 10 ? 'short' : 'long'),
label: 'length',
);
expect(s.split('').reversed.join().split('').reversed.join(), equals(s));
}, verbosity: Verbosity.verbose);
When run with verbosity: Verbosity.verbose, distribution percentages are printed at the end of the test. When running programmatically with runHegelFlutterTest(), you can inspect result.statistics directly or format it with result.formatStatistics().
Persistent Counterexample Database
By default, hegeltest_flutter automatically caches discovered failing counterexamples to .hegel/examples/ (scoped automatically by the test's description). On subsequent test runs, known failing examples are replayed first on iteration 1 during Phase.reuse, providing instant regression feedback before generating fresh random inputs.
To ensure your repository worktree stays clean, hegeltest automatically generates a .gitignore inside .hegel/.
You can configure or disable persistence:
- Opt-out: pass
database: falseor set the environment variableHEGEL_DATABASE=0to disable disk persistence and replay. Explicitdatabasearguments orHegelConfig.databasesettings take precedence overHEGEL_DATABASE. - Custom storage path: pass
databasePath: '.custom_db/'to store counterexamples in an alternative directory. - Stable scoping: pass
databaseKey: 'my_stable_key'to preserve cache continuity even if a test description changes.
In CI pipelines (e.g. GitHub Actions), cache .hegel/ to catch regressions from previous runs instantly:
- name: Cache Hegel counterexamples
uses: actions/cache@v4
with:
path: .hegel/
key: hegel-${{ runner.os }}-${{ github.ref_name }}
restore-keys: hegel-${{ runner.os }}-
Platform Support
| Platform | Status |
|---|---|
| macOS arm64 | ✅ |
| Linux x64 | ✅ |
| Linux arm64 | ✅ |
| Windows x64 | ✅ |
| Windows arm64 | ✅ |
| Web | ❌ (throws UnsupportedError) |
License
MIT. See LICENSE.
Libraries
- hegeltest_flutter
- Flutter integration for hegeltest — property-based testing for Flutter.