This project uses BDD (Behavior-Driven Development) style tests with TestDriver abstraction.
BDD (Gherkin .feature) scenarios produce hand-written Patrol E2E tests only
โ no code
generator, no widget tests. See cc-flutter/skills/patrol-bdd-conventions
for the full rationale.
Test Hierarchy#
| Level | Tool | Location | Scope | CI |
|---|---|---|---|---|
| Patrol E2E Test (BDD scenarios) | patrol (hand-written) | app/*/integration_test/scenarios/ |
Cross-feature, real server | schedule (nightly) + workflow_dispatch |
| Golden Test | alchemist / golden_toolkit | feature/*/test/goldens/ |
Visual regression | PR changes |
โ "BDD Widget Test" is retired. .feature files never generate *.widget_test.dart
โ in
fact they never generate anything; there is no builder in this pipeline anymore. Non-BDD widget
tests (plain testWidgets, golden, BLoC widget tests unrelated to Gherkin) are hand-written,
unaffected, and still run on every push โ see cc-flutter/skills/widget-testing.
TestDriver Abstraction#
A .feature file is a spec. A human copies its Scenario/step names into a hand-written Patrol test:
app/{app}/integration_test/features/*.feature (spec, not a build target)
โโโ app/{app}/integration_test/scenarios/*_test.dart (hand-written, TestDriver + PatrolTestDriver)
Step functions: TestDriver-based, hand-written
K class: Unified widget Keys โ must be assigned to actual widgets
Key Classes#
| Class | Purpose | Wraps |
|---|---|---|
TestDriver | Abstract interface | โ |
PatrolTestDriver |
E2E test driver (only implementation) | PatrolIntegrationTester |
K | Centralized widget Keys | โ |
Directory Structure#
โ There is no BDD scaffold inside a feature package. No .feature, no test/src/bdd/, no
builder block in that package's build.yaml. Everything BDD-related lives under one app-level
directory:
app/{app}/integration_test/
โโโ features/{name}.feature # Gherkin scenarios, canonical (not a build target)
โโโ helpers/screen_batch.dart # runScreenBatch(...) batch runner (hand-written)
โโโ step/ # Step functions (TestDriver-based, hand-written)
โ โโโ i_am_on_the_{page}.dart
โ โโโ i_tap_the_{button}.dart
โ โโโ the_{element}_should_be_displayed.dart
โ โโโ the_{screen}_screen_is_reset.dart # per-screen reset for batches
โโโ scenarios/ # Patrol tests (hand-written, canonical shared_step)
โโโ {name}_{screen}_batch_test.dart # default: 3+ scenarios sharing one Background
โโโ {name}_{scenario}_test.dart # only when @isolated, or fewer than 3 scenarios
A feature package (feature/{type}/{name}/) only ever has ordinary, non-BDD widget tests
(test/src/presentation/widget/*_test.dart, plain testWidgets) โ see
widget-testing.
โ Legacy (bdd_widget_test) and dual-generation (bdd_test_gen/co_test_gen) are both
retired, not just deprecated. If you find a test/src/bdd/ scaffold (.feature
+ hooks/ +
generated _test.dart/.patrol_test.dart + a builder block in
build.yaml) still inside a
feature package, delete the whole scaffold: move the scenario to a hand-written Patrol
scenarios/*_test.dart under app/{app}/integration_test/, and move any widget assertion into
an ordinary hand-written widget test in the feature package. Don't leave the generator running.
Step File Naming Rules#
Given (Preconditions)#
i_am_on_the_{page}.dart- On a specific pagethe_dashboard_has_loaded.dart- Dashboard has loadedthe_filter_data_is_available.dart- Filter data is available
When (Actions)#
i_tap_the_{button}.dart- Tap buttoni_select_{option}.dart- Select optioni_enter_{input}.dart- Enter input value
Then (Result Verification)#
the_{element}_should_be_displayed.dart- Element is displayedthe_{value}_should_be_{expected}.dart- Value matches expectedthe_{error}_should_be_displayed.dart- Error is displayed
Step Function Patterns#
TestDriver-based (only supported form for BDD steps)#
import 'package:test_driver/test_driver.dart';
/// Usage: I enter {string} in the email field # ์ด๋ฉ์ผ ํ๋์ ์
๋ ฅ
Future<void> iEnterInTheEmailField(TestDriver driver, String param1) async {
await driver.enterText(K.emailField, param1);
}
/// Usage: the book list should be displayed # ๋์ ๋ชฉ๋ก ํ์
Future<void> theBookListShouldBeDisplayed(TestDriver driver) async {
await driver.expectVisible(K.bookList);
}
โ WidgetTester-based step functions are retired for BDD. A step whose first parameter is
WidgetTester (not TestDriver) is either non-BDD widget-test code (belongs in
cc-flutter/skills/widget-testing, not here) or a leftover from the retired
bdd_widget_test
scaffold that should be migrated to a TestDriver-based step and moved under Patrol.
Patrol Scenario File Structure#
Patrol E2E is the most expensive test in this pipeline, and most of the cost is arrival
โ
installing the bundle, cold-starting the app, logging in, navigating to the screen. Scenarios
that share a Background therefore share one arrival: they go into a screen batch
file and
run back-to-back in a single session, with a screen reset between them.
Batch file (default โ 3+ scenarios sharing one Background)
// scenarios/sales_sales_analysis_batch_test.dart
void main() {
patrolTest(
'''Sales Analysis โ batch (3 scenarios)''',
config: patrolConfig,
($) async {
final driver = PatrolTestDriver($);
// Background โ once per batch (deliberate deviation from standard Gherkin)
await theAppIsInitialized($);
await iAmOnTheSalesAnalysisPage(driver);
await theDashboardHasLoaded(driver);
await runScreenBatch(
$,
screen: 'sales_analysis',
reset: () => theSalesAnalysisScreenIsReset($, driver),
scenarios: {
// keys are the .feature Scenario titles, verbatim
'Select 7 days period': () async {
await iSelect7DaysPeriod(driver);
await the7DaysPeriodShouldBeSelected(driver);
},
'Select 30 days period': () async {
await iSelect30DaysPeriod(driver);
await the30DaysPeriodShouldBeSelected(driver);
},
'Export button is enabled once data loads': () async {
await theWidgetShouldBeDisplayed(driver, 'sales_export_button');
},
},
);
},
);
}
Single-scenario file (@isolated, or fewer than 3 scenarios)
// scenarios/sales_export_download_test.dart (@isolated โ writes a file to the device)
void main() {
patrolTest(
'''Exporting downloads a CSV''',
config: patrolConfig,
($) async {
final driver = PatrolTestDriver($);
await theAppIsInitialized($);
await iAmOnTheSalesAnalysisPage(driver);
await iTapTheWidget(driver, 'sales_export_button');
await theWidgetShouldBeDisplayed(driver, 'sales_export_success');
},
);
}
The batching unit, the reset contract, the @isolated criteria and the FAIL/BLOCKED reporting
rules are canonical in cc-flutter/skills/patrol-bdd-conventions
โ "ํ๋ฉด ๋จ์ ๋ฐฐ์น".
Don't restate them here.
Step File Creation Checklist#
- Create file:
app/{app}/integration_test/step/{step_name}.dart -
Add import: Import it from the
scenarios/*_test.dartfile(s) that use it - Implement function: Implement appropriate verification/behavior logic
-
Run tests:
patrol test --target integration_test/scenarios/{name}_{screen}_batch_test.dart
For a reset step (the_{screen}_screen_is_reset.dart) the implementation must satisfy all
three obligations โ dismiss any open overlay, restore in-screen state, and assert
the arrival
anchor Key is visible again. A reset without the assertion is hope, not a reset.
Missing Step File Error#
error โข Target of URI doesn ' t exist: ' ./step/the_kpi_card_section_should_be_displayed.dart '
Solution:
- Create the missing step file
- Implement function following standard patterns
- Re-run analysis to verify
Scenario Target Tags#
Scenario: Native home button press # ๋ค์ดํฐ๋ธ ํ ๋ฒํผ
When I press the home button
And I return to the app
Then the session should be restored
โ @both/@widget-only/@patrol-only are retired.
Every scenario maps to a hand-written
Patrol E2E test โ there is no widget target to select. Grading/scheduling tags (@smoke,
@P0โ@P2, domain tags) are unaffected and still apply.
๐ @isolated is the one structural tag. It excludes a scenario from its screen batch โ
required when the scenario changes auth state, makes irreversible server state, leaves the screen
for good (@journey), or depends on an app restart. Untagged scenarios are batched by default.
Full criteria: cc-flutter/skills/patrol-bdd-conventions โ "ํ๋ฉด ๋จ์ ๋ฐฐ์น".
BDD Scenarios Are Patrol-Only โ No Selection Table#
There used to be a "Widget Test vs Patrol E2E" comparison here for choosing a target per
scenario. That choice no longer exists: every .feature scenario is a Patrol E2E test. A fast,
deterministic, mock-backed check that isn't a Gherkin scenario is a non-BDD
widget test โ
see cc-flutter/skills/widget-testing, a separate and unaffected test type.
Precautions#
- File name = Function name: File and function names must match (snake_case to camelCase conversion)
-
Nothing is auto-generated:
scenarios/*_test.dartis hand-written โ edit it directly when the.featurespec changes, there's no generator to re-run - async/sync: TestDriver-based steps always use
Future<void>async -
Korean comment stripping in
.feature:#comments are documentation only, for humans โ no parser consumes them since there's no generator
Related Documents#
- Flutter test documentation
-
See the
bdd-testingskill in this plugin (skills/bdd-testing) for detailed TestDriver patterns