gherkart 0.2.1
gherkart: ^0.2.1 copied to clipboard
A lightweight, runtime BDD framework for Dart and Flutter testing. Parse Gherkin .feature files at runtime with no code generation.
Gherkart #
A lightweight, runtime BDD framework for Dart and Flutter testing.
Parse Gherkin .feature files at runtime — no code generation required.
Features at a Glance #
Gherkin / Cucumber Standard #
| Feature | Description |
|---|---|
| Scenarios & Background | Scenario:, Background: with Given / When / Then steps |
| Scenario Outlines | Scenario Outline: with Examples: tables (<param> and {param} syntax) |
| Data Tables | Step-attached structured tables |
| Doc Strings | Multi-line """ text blocks on steps |
| Tags | @smoke, @wip, @slow etc. with feature → scenario inheritance |
| Missing step detection | Reports unregistered steps with source locations |
Gherkart Improvements #
| Feature | Description |
|---|---|
| Runtime parsing | Reads .feature files at test time, no code generation or build step |
| Framework-agnostic | Pluggable TestAdapter for test, flutter_test, Patrol, or any runner |
| Typed parameters | {name} placeholders with auto-conversion to int, double, bool |
| Multiple sources | FileSystemSource (disk) or AssetSource (in-memory / web) |
| Registry merging | Compose step registries from separate modules |
| Scheme resolution | {t:translationKey} parameter schemes for i18n and key lookup |
| Translation handlers | ARB file, map, or function-based lookup with parameterized values |
| Configurable output | Silent → scenario names → steps → verbose with timing |
| Reporting system | SummaryReporter, BufferedReporter, MarkdownFileReporter, composable |
| Test structure | Tree (nested by directory) or flat grouping |
| Lifecycle hooks | beforeAll, afterAll, beforeEach, afterEach with tag access |
Quick Start #
Full example: example/demo_test.dart + example/features/demo.feature
1. Add dependency #
# pubspec.yaml
dev_dependencies:
gherkart:
path: packages/gherkart # or from pub.dev
2. Write a feature file #
# example/features/demo.feature
Feature: BDD Framework Demo
A simple demo to verify the BDD framework works.
Scenario: Simple math
Given I have the number 5
When I add 3
Then the result is 8
3. Define steps #
final mathSteps = StepRegistry<void>.fromMap({
'I have the number {n}'.mapper(types: {'n': int}): ($, ctx) async {
_state.number = ctx.arg<int>(0);
},
'I add {n}'.mapper(types: {'n': int}): ($, ctx) async {
_state.number += ctx.arg<int>(0);
},
'the result is {expected}'.mapper(types: {'expected': int}): ($, ctx) async {
expect(_state.number, ctx.arg<int>(0));
},
});
4. Run tests #
import 'package:gherkart/gherkart.dart';
import 'package:gherkart/gherkart_io.dart';
import 'package:test/test.dart';
void main() async {
await runBddTests<void>(
rootPaths: ['example/features/demo.feature'],
registry: mathSteps,
source: FileSystemSource(),
adapter: _createTestAdapter(),
output: const BddOutput.steps(),
);
}
Usage with Flutter & Patrol #
import 'package:gherkart/gherkart.dart';
import 'package:gherkart/gherkart_io.dart';
import 'package:patrol_finders/patrol_finders.dart';
void main() async {
await runBddTests<PatrolTester>(
rootPaths: ['test/features'],
registry: myStepRegistry,
source: FileSystemSource(),
adapter: TestAdapter<PatrolTester>(
testFunction: (name, {tags, skip = false, required callback}) {
patrolWidgetTest(name, tags: tags, skip: skip, ($) => callback($));
},
group: group,
setUpAll: setUpAll,
tearDownAll: tearDownAll,
fail: (message) => fail(message),
),
output: const BddOutput.steps(),
);
}
Core #
Gherkin Parser #
The parser supports the full Gherkin syntax relevant to testing.
Scenarios & Background #
Example: example/features/background.feature
Feature: Background Demo
Demonstrates that Background steps run before each scenario.
Background:
Given I have the number 10
Scenario: Add with background
When I add 5
Then the result is 15
Background steps are prepended to every scenario in the feature.
Scenario Outlines #
Example: example/features/outline.feature
Scenario Outline: Addition with examples
Given I have the number <a>
When I add <b>
Then the result is <sum>
Examples: Small numbers
| a | b | sum |
| 1 | 2 | 3 |
| 5 | 3 | 8 |
Examples: Larger numbers
| a | b | sum |
| 100 | 200 | 300 |
| 999 | 1 | 1000 |
Each example row becomes a separate test case. Both <placeholder> and {placeholder} syntax are supported in step text.
Data Tables #
Example: example/features/data_tables.feature + example/data_tables_test.dart
Scenario: Add items from a table
Given I have an empty inventory
When I add items:
| name | quantity |
| Apples | 5 |
| Oranges | 3 |
| Bananas | 2 |
Then the total quantity is 10
Access in step code via ctx.tableRows:
'I add items:'.mapper(): ($, ctx) async {
for (final row in ctx.tableRows) {
final name = row['name']!;
final qty = int.parse(row['quantity']!);
inventory[name] = (inventory[name] ?? 0) + qty;
}
},
Doc Strings #
Example: example/features/doc_strings.feature + example/data_tables_test.dart
Scenario: Parse JSON configuration
Given the configuration:
"""json
{
"theme": "dark",
"fontSize": 14
}
"""
Then the theme is "dark"
And the font size is 14
Access via ctx.docContent:
'the configuration:'.mapper(): ($, ctx) async {
final config = json.decode(ctx.docContent) as Map<String, dynamic>;
// ...
},
Tags #
Example: example/features/tagged.feature
@smoke
Feature: Tagged Feature Demo
Scenario: Regular scenario inherits feature tags
Given I have the number 10
When I add 5
Then the result is 15
@wip
Scenario: WIP scenario should be skippable
Given I have the number 100
When I add 50
Then the result is 150
- Feature tags are inherited by all scenarios
@wipscenarios are automatically skipped- Tags are passed to the test framework for filtering (
--tags smoke)
Step Registry #
Example: example/demo_test.dart
Pattern Matching #
Steps are matched by pattern with {param} placeholders:
final mathSteps = StepRegistry<void>.fromMap({
'I have the number {n}'.mapper(types: {'n': int}): ($, ctx) async {
_state.number = ctx.arg<int>(0);
},
'I add {n}'.mapper(types: {'n': int}): ($, ctx) async {
_state.number += ctx.arg<int>(0);
},
'the result is {expected}'.mapper(types: {'expected': int}): ($, ctx) async {
expect(_state.number, ctx.arg<int>(0));
},
});
Typed Parameters #
The .mapper() extension accepts a types map for automatic conversion:
| Type | Example input | Parsed as |
|---|---|---|
String |
"hello" |
'hello' |
int |
42 |
42 |
double |
3.14 |
3.14 |
bool |
true |
true |
Merging Registries #
Example: example/data_tables_test.dart — merges
inventorySteps+configSteps
Compose step definitions from separate modules:
final inventorySteps = StepRegistry<void>.fromMap({ ... });
final configSteps = StepRegistry<void>.fromMap({ ... });
final allSteps = inventorySteps.merge(configSteps);
Missing Step Detection #
When a feature file references an unregistered step, the runner reports all missing steps with source file locations and generates ready-to-paste placeholder code:
MissingStepsException: 2 step(s) not found:
- test/features/login.feature:12: I enter valid credentials
- test/features/login.feature:13: I should see the dashboard
Feature Sources #
FileSystemSource #
Reads .feature files from disk. Requires dart:io — import from gherkart_io.dart:
import 'package:gherkart/gherkart_io.dart';
final source = FileSystemSource();
AssetSource #
In-memory source for web or bundled assets:
// From a map
final source = AssetSource.fromMap({
'login.feature': 'Feature: Login\n Scenario: ...',
});
// From a loader function (e.g. rootBundle)
final source = AssetSource.fromLoader((path) async {
return await rootBundle.loadString('assets/$path');
});
Add-ons #
Scheme Resolution #
Example: example/scheme_test.dart + example/features/scheme.feature
Parameterized example: example/parameterized_translation_test.dart + example/features/parameterized_translation.feature
Plural example (map): example/plural_translation_test.dart + example/features/plural_translation.feature
Plural example (ARB): example/plural_translation_arb_test.dart + example/features/plural_translation_arb.feature
Registering Scheme Handlers #
At its simplest, a scheme handler is just an async function that receives a key and a parameter map:
final resolver = SchemeResolver()
..register('x', (String key, Map<String, String> params) async {
return [key, ...params.values].join(' ');
});
# Resolves to "hello 1 World"
Then "{x:hello(p1: 1, p2: 'World')}" is "hello 1 World"
Pass the resolver to runBddTests:
await runBddTests<void>(
// ...
schemeResolver: resolver,
);
Built-in Translation Handlers #
| Handler | Source |
|---|---|
createArbTranslationHandler |
ARB file (via FeatureSource) |
createMapTranslationHandler |
In-memory Map<String, String> |
createTranslationHandler |
Sync lookup function |
createKeyMappingHandler |
Widget key name → value |
Scheme prefixes in feature file parameters are resolved before they reach step functions:
Then "{t:hello}" is "Hello, World!" # Simple key lookup
Then "{t:welcome(name: 'Alice')}" is "Welcome, Alice!" # Parameterized
Then "{t:greeting(name: 'Alice', time: 'morning')}" is "Good morning, Alice!"
Then "plain text" is "plain text" # Literal (no scheme)
When parameters are provided with {t:key(param: value)} syntax, the key
and a Map<String, String> of parameters are passed to the scheme handler,
which resolves the final value. String values should be single-quoted;
unquoted values (like numbers) are kept as-is.
With an in-memory map:
final resolver = SchemeResolver()
..register(
't',
createMapTranslationHandler({
'hello': 'Hello, World!',
'welcome': 'Welcome, {name}!',
'greeting': 'Good {time}, {name}!',
}),
);
Or from an ARB file:
{
"@@locale": "en",
"hello": "Hello, World!",
"welcome": "Welcome, {name}!",
"@welcome": {
"placeholders": {
"name": { "type": "String" }
}
},
"greeting": "Good {time}, {name}!",
"@greeting": {
"placeholders": {
"name": { "type": "String" },
"time": { "type": "String" }
}
}
}
final resolver = SchemeResolver()
..register('t', createArbTranslationHandler('lib/l10n/en.arb'));
ICU Plural Support #
The built-in translation handlers (createMapTranslationHandler and
createArbTranslationHandler) support ICU MessageFormat plural syntax:
# Feature file
Then "{t:shotLabel(count: 0)}" is "no shots"
Then "{t:shotLabel(count: 1)}" is "1 shot"
Then "{t:shotLabel(count: 5)}" is "5 shots"
final resolver = SchemeResolver()
..register(
't',
createMapTranslationHandler({
'shotLabel': '{count, plural, =0{no shots} =1{1 shot} other{{count} shots}}',
}),
);
Or equivalently in an ARB file:
{
"@@locale": "en",
"shotLabel": "{count, plural, =0{no shots} =1{1 shot} other{{count} shots}}",
"@shotLabel": {
"placeholders": {
"count": { "type": "int" }
}
}
}
Supported plural features:
| Syntax | Description |
|---|---|
=0, =1, =N |
Exact numeric match |
other |
Fallback when no exact match is found |
{param} |
Substituted with the parameter value inside a plural branch |
# |
Shorthand for the plural parameter's value |
Plurals can be mixed with regular {param} placeholders:
// "{t:userShots(name: 'Alice', count: 2)}" → "Alice scored 2 shots"
'userShots': '{name} scored {count, plural, =0{nothing} =1{1 shot} other{{count} shots}}',
Using Scheme Handlers in Widget Tests #
The Problem
testWidgets (and patrolWidgetTest) runs test callbacks inside Flutter's FakeAsync zone. Real dart:io operations like File.readAsString() never complete inside FakeAsync because it doesn't process the real I/O event loop.
Scheme handlers are called during step execution — inside the test callback, inside FakeAsync. Any handler that performs file I/O (such as createArbTranslationHandler, which lazily reads the ARB file on first {t:key} resolution) will cause an infinite hang.
Note: Feature file reading happens in main() via runBddTests / BddTestRunner.run(), so FileSystemSource is not affected.
Recommended Pattern
Pre-read any files in main() (which runs outside FakeAsync) and pass the data to your scheme handler:
import 'dart:convert';
import 'dart:io';
Future<void> main() async {
// Read ARB here in main() — outside the FakeAsync zone.
// testWidgets callbacks run inside FakeAsync where dart:io never completes.
final arbContent = await File('lib/l10n/en.arb').readAsString();
final arbMap = (json.decode(arbContent) as Map<String, dynamic>)
.cast<String, String>();
final schemeResolver = SchemeResolver()
..register('t', createMapTranslationHandler(arbMap));
await runBddTests(
schemeResolver: schemeResolver,
// ...
);
}
Why not createArbTranslationHandler?
| Handler | Reads file | Safe in FakeAsync |
|---|---|---|
createArbTranslationHandler |
Lazily, on first {t:key} use |
No — hangs forever |
createMapTranslationHandler |
Never (you provide the map) | Yes |
createArbTranslationHandler is fine for CLI tools or integration tests that run in a real async zone. For widget tests, always use createMapTranslationHandler with a pre-loaded map.
The same applies to any custom scheme handler: if it does file I/O, do the reading in main() and pass the result into the handler.
Rule of thumb
Any dart:io in main() → safe. Any dart:io inside a scheme handler or test callback → hangs in FakeAsync.
Output Verbosity #
Example: example/data_tables_test.dart uses
BddOutput.verbose(), example/demo_test.dart usesBddOutput.steps()
Control what prints during test execution:
| Preset | Feature names | Scenario names | Steps | Timing |
|---|---|---|---|---|
BddOutput.none |
||||
BddOutput.scenarios() |
✓ | ✓ | ||
BddOutput.steps() |
✓ | ✓ | ✓ | |
BddOutput.verbose() |
✓ | ✓ | ✓ | ✓ |
await runBddTests<void>(
// ...
output: const BddOutput.verbose(),
);
Example verbose output:
📋 Feature: User Login
🎬 Scenario: Valid login
✓ Given the app is running (42ms)
✓ When I enter valid credentials (128ms)
✓ Then I see the dashboard (15ms)
Test Structure #
Example: example/reporter_test.dart uses
TestStructure.tree, example/demo_test.dart usesTestStructure.flat
Control how test groups are organized:
| Structure | Description |
|---|---|
TestStructure.flat |
One group() per feature, no folder nesting |
TestStructure.tree |
Groups nested by folder structure (default) |
await runBddTests<void>(
// ...
structure: TestStructure.tree,
);
Reporters #
Example: example/reporter_test.dart
Reporters collect structured results from test execution for post-processing.
Built-in Reporters #
| Reporter | Behavior |
|---|---|
ContinuousReporter |
Prints events as they happen |
BufferedReporter |
Collects all events, reports on flush() |
SummaryReporter |
Prints pass/fail/skip counts |
CompositeReporter |
Combines multiple reporters |
MarkdownFileReporter |
Writes per-feature Markdown files with results |
Markdown Report Generation #
Generate human-readable feature documentation from test results:
final reporter = MarkdownFileReporter(
outputDir: 'build/docs/features',
cleanFirst: true,
);
await runBddTests<PatrolTester>(
// ...
reporter: reporter,
);
This produces Markdown files mirroring your feature directory structure, with pass/fail status for each scenario and step.
Lifecycle Hooks #
Example: example/reporter_test.dart
await runBddTests<PatrolTester>(
// ...
hooks: BddHooks(
beforeAll: () async { /* one-time setup */ },
afterAll: () async { /* one-time teardown */ },
beforeEach: (scenarioName, tags) async { /* per-scenario setup */ },
afterEach: (scenarioName, success, tags) async { /* per-scenario teardown */ },
),
);
API Reference #
Libraries #
| Library | Description |
|---|---|
gherkart.dart |
Core API — parser, runner, registry, reporters |
gherkart_io.dart |
FileSystemSource (requires dart:io) |
Key Types #
| Type | Purpose |
|---|---|
StepRegistry<T> |
Maps step patterns to functions |
StepContext |
Provides args, table, docString to step functions |
LineMapper |
Pattern matcher created via String.mapper() |
BddTestRunner<T> |
Orchestrates discovery, parsing, and execution |
TestAdapter<T> |
Plugs in any test framework |
FeatureSource |
Abstraction for reading feature files |
SchemeResolver |
Transforms parameter values via scheme handlers |
BddOutput |
Controls console output verbosity |
BddReporter |
Interface for structured test result reporting |
BddHooks<T> |
Lifecycle callbacks for setup/teardown |
Feature |
Parsed Gherkin feature with scenarios |
Scenario |
Parsed scenario with steps |
ScenarioOutline |
Template scenario expanded via Examples tables |
DataTable |
Structured table data attached to a step |
DocString |
Multi-line string attached to a step |
TestPlan |
Organized test groups ready for execution |
FeatureTestFactory |
Builds test plans from parsed features |
MarkdownFileReporter |
Generates Markdown docs from test results |
Development #
# Run all tests (unit + examples)
just test-all
# Run unit tests only
just test-unit
# Run all examples
just test-example
# Run individual examples
just test-example-demo # scenarios, background, tags, outlines
just test-example-data # data tables, doc strings, registry merging
just test-example-scheme # scheme resolution, translation handlers
just test-example-reporter # reporters, lifecycle hooks, tree structure
# Static analysis
just analyze
# Format code
just format
# All checks before committing
just check
Commit messages #
Use the format:
feat/fix/refactor: Short description
Somewhat longer description when needed.
Branches and pull requests #
This project follows the git flow branching model by Vincent Driessen, using the git-flow AVH tool.
The default branch is develop. Pull request branches should be based on develop and named:
feature/*— new features
CI runs on all pushes to main, develop, feature/*, hotfix/*, and release/*.
On pull requests, coverage is checked and must not decrease.
License #
MIT — see LICENSE.