LogoSkills

BDD Test Patterns

This project uses BDD (Behavior-Driven Development) style tests with TestDriver abstraction.

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#

LevelToolLocationScopeCI
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#

ClassPurposeWraps
TestDriverAbstract interfaceโ€”
PatrolTestDriver E2E test driver (only implementation) PatrolIntegrationTester
KCentralized 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 page
  • the_dashboard_has_loaded.dart - Dashboard has loaded
  • the_filter_data_is_available.dart - Filter data is available

When (Actions)#

  • i_tap_the_{button}.dart - Tap button
  • i_select_{option}.dart - Select option
  • i_enter_{input}.dart - Enter input value

Then (Result Verification)#

  • the_{element}_should_be_displayed.dart - Element is displayed
  • the_{value}_should_be_{expected}.dart - Value matches expected
  • the_{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#

  1. Create file: app/{app}/integration_test/step/{step_name}.dart
  2. Add import: Import it from the scenarios/*_test.dart file(s) that use it
  3. Implement function: Implement appropriate verification/behavior logic
  4. 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:

  1. Create the missing step file
  2. Implement function following standard patterns
  3. 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#

  1. File name = Function name: File and function names must match (snake_case to camelCase conversion)
  2. Nothing is auto-generated: scenarios/*_test.dart is hand-written โ€” edit it directly when the .feature spec changes, there's no generator to re-run
  3. async/sync: TestDriver-based steps always use Future<void> async
  4. Korean comment stripping in .feature: # comments are documentation only, for humans โ€” no parser consumes them since there's no generator