LogoSkills

BDD Test Patterns

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

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

Test Hierarchy#

LevelToolLocationScopeCI
BDD Widget Test bdd_test_gen / bdd_widget_test feature/*/test/src/bdd/ Single feature, mock-based Every push
Patrol E2E Test patrol + bdd_test_gen app/*/integration_test/ Cross-feature, real server PR gate + Nightly
Golden Test alchemist / golden_toolkit feature/*/test/goldens/ Visual regression PR changes

Generate both widget tests and Patrol E2E tests from a single .feature file:

*.feature → bdd_test_gen Builder (build_runner)
    ├── *.widget_test.dart   (TestDriver + WidgetTestDriver)
    └── *.patrol_test.dart   (TestDriver + PatrolTestDriver)

Step functions: TestDriver-based → reusable in both
K class: Unified widget Keys → must be assigned to actual widgets

Key Classes#

ClassPurposeWraps
TestDriverAbstract interface—
WidgetTestDriverWidget test driverWidgetTester
PatrolTestDriver E2E test driver PatrolIntegrationTester
KCentralized widget Keys—

Directory Structure#

feature/{type}/{name}/test/src/bdd/
├── {name}.feature              # Gherkin scenarios
├── {name}.widget_test.dart     # Auto-generated (DO NOT MODIFY)
├── {name}.patrol_test.dart     # Auto-generated (DO NOT MODIFY)
└── step/                       # Step functions (TestDriver-based)
    ├── i_am_on_the_{page}.dart
    ├── i_tap_the_{button}.dart
    └── the_{element}_should_be_displayed.dart

Legacy (bdd_widget_test)#

feature/{module}/test/src/bdd/
├── {feature}_test.dart       # Auto-generated
├── hooks/
│   └── hooks.dart            # Test setup/teardown hooks
└── step/
    ├── i_am_on_the_{page}.dart
    ├── i_tap_the_{button}.dart
    ├── the_{element}_should_be_displayed.dart
    └── ...

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

build.yaml Configuration#

targets:
  $default:
    builders:
      bdd_test_gen|dual_test_gen:
        enabled: true
        generate_for:
          - test/src/bdd/*.feature
        options:
          stepFolder: step

bdd_widget_test (legacy)#

targets:
  $default:
    builders:
      bdd_widget_test|featureBuilder:
        enabled: true
        generate_for:
          - test/src/bdd/*.feature
        options:
          stepFolders:
            - test/src/bdd/step

Step Function Patterns#

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 (Legacy)#

import 'package:flutter_test/flutter_test.dart';

/// KPI card section should be displayed
void theKpiCardSectionShouldBeDisplayed(WidgetTester tester) {
  expect(find.byType(SalesKpiSection), findsOneWidget);
}

Asynchronous Function (When Needed)#

import 'package:flutter_test/flutter_test.dart';

/// Tap button
Future<void> iTapTheSubmitButton(WidgetTester tester) async {
  await tester.tap(find.text('Submit'));
  await tester.pumpAndSettle();
}

When Parameter is Unused#

/// Dashboard has loaded (tester unused)
void theDashboardHasLoaded(WidgetTester _) {
  // Only perform state setup
}

Test File Structure#

@Tags(['smoke', 'sales_analysis'])
import 'package:flutter_test/flutter_test.dart';

import './step/i_am_on_the_sales_analysis_page.dart';
import './step/the_dashboard_has_loaded.dart';
import './step/i_select_7_days_period.dart';
import './step/the_7_days_period_should_be_selected.dart';

void main() {
  group('''Sales Analysis''', () {
    Future<void> bddSetUp(WidgetTester tester) async {
      await iAmOnTheSalesAnalysisPage(tester);
    }

    testWidgets('''Select 7 days period''', (tester) async {
      await bddSetUp(tester);
      theDashboardHasLoaded(tester);
      await iSelect7DaysPeriod(tester);
      the7DaysPeriodShouldBeSelected(tester);
    }, tags: ['period_selection']);
  });
}

Step File Creation Checklist#

  1. Create file: test/src/bdd/step/{step_name}.dart
  2. Add import: Add import to main test file
  3. Implement function: Implement appropriate verification/behavior logic
  4. Run tests: flutter test or melos run test

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 (bdd_test_gen)#

   @widget-only
  Scenario: Mock-based loading state # Mock ęļ°ë°˜ 로ë”Đ ėƒíƒœ
    Given the server returns loading state
    Then the loading indicator should be displayed

  @patrol-only
  Scenario: Native home button press # ë„Īėī티ëļŒ í™ˆ ëē„튞
    When I press the home button
    And I return to the app
    Then the session should be restored
  • @both (default) — generates both widget + Patrol tests
  • @widget-only — widget test only (skipped in Patrol)
  • @patrol-only — Patrol E2E only (skipped in widget test)

Widget Test vs Patrol E2E Selection#

CriteriaBDD Widget TestPatrol E2E
ScopeSingle feature/screenCross-feature flow
BackendMockReal server (Staging)
SpeedFast (milliseconds)Slow (minutes)
StabilityHigh (deterministic)Lower (network-dependent)
NativeNot availableAvailable (home, notifications)

Precautions#

  1. File name = Function name: File and function names must match (snake_case to camelCase conversion)
  2. Auto-generated code: *.widget_test.dart, *.patrol_test.dart, *_test.dart — DO NOT MODIFY
  3. async/sync: TestDriver-based steps always use Future<void> async
  4. pumpAndSettle caution: Use pump() in WidgetTestDriver (avoids null check errors)
  5. Korean comment stripping: # comments in step patterns are auto-removed by parser