LogoSkills

bdd-test-patterns

BDD widget test patterns with bdd_widget_test and TestApp hooks. Use when writing BDD widget tests with bdd_widget_test and TestApp hooks.

BDD ํ…Œ์ŠคํŠธ ํŒจํ„ด#

ํ•œ๋งˆ๋””๋กœ (๋น„๊ฐœ๋ฐœ์ž์šฉ)#

์•ฑ ํ™”๋ฉด์ด "์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ €๋ ‡๊ฒŒ ๋ณด์—ฌ์•ผ ํ•œ๋‹ค"๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค๋Œ€๋กœ ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ์ž๋™์œผ๋กœ ์ ๊ฒ€ํ•ด ์ฃผ๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ๋งˆ์น˜ "์Šคํ† ์–ด ํŽ˜์ด์ง€์—์„œ ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์ฑ… ๋ชฉ๋ก์ด ๋‚˜์™€์•ผ ํ•œ๋‹ค" ๊ฐ™์€ ์ผ์ƒ ๋ฌธ์žฅ์„ ๊ทธ๋Œ€๋กœ ์ ์–ด ๋‘๋ฉด, ์ปดํ“จํ„ฐ๊ฐ€ ๋งค๋ฒˆ ์ง์ ‘ ๋ˆŒ๋Ÿฌ ๋ณด๋ฉฐ ํ™•์ธํ•ด ์ฃผ๋Š” ์ž๋™ ํ’ˆ์งˆ๊ฒ€์‚ฌ ์ง์›์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์‚ฌ๋žŒ์ด ์†์œผ๋กœ ํ•˜๋‚˜ํ•˜๋‚˜ ํ…Œ์ŠคํŠธํ•  ํ•„์š” ์—†์ด, ์ •ํ•ด ๋‘” ์‹œ๋‚˜๋ฆฌ์˜ค๋Œ€๋กœ ํ™”๋ฉด์ด ๋™์ž‘ํ•˜๋Š”์ง€ ๋ฐ˜๋ณต ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

๋ฌด์—‡์„ยท์–ธ์ œ (๋น„๊ฐœ๋ฐœ์ž์šฉ)#

  • ๋ฌด์—‡์„ ํ•ด์ฃผ๋‚˜์š”: ์ผ์ƒ์–ด์— ๊ฐ€๊นŒ์šด ์‹œ๋‚˜๋ฆฌ์˜ค(.feature ํŒŒ์ผ)๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ Flutter ์•ฑ ํ™”๋ฉด์„ ์ž๋™์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ค์–ด ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. "์–ด๋–ค ์ƒํ™ฉ์—์„œ(Given) โ†’ ๋ฌด์—‡์„ ํ•˜๋ฉด(When) โ†’ ์–ด๋–ป๊ฒŒ ๋˜์–ด์•ผ ํ•œ๋‹ค(Then)" ํ˜•์‹์œผ๋กœ ํ™”๋ฉด ๋™์ž‘์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.
  • ์–ธ์ œ ์“ฐ์ด๋‚˜์š”: ์ƒˆ ํ™”๋ฉด์ด๋‚˜ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค๋ฉด์„œ "์ด ํ™”๋ฉด์ด ์˜๋„๋Œ€๋กœ ๋ณด์ด๊ณ  ๋™์ž‘ํ•˜๋Š”์ง€"๋ฅผ ์‚ฌ๋žŒ ์†์ด ์•„๋‹ˆ๋ผ ์ž๋™์œผ๋กœ ํ™•์ธํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  • ์™œ ์ข‹์€๊ฐ€์š”: ์‹œ๋‚˜๋ฆฌ์˜ค๋งŒ ๊ธ€๋กœ ์ ์–ด ๋‘๋ฉด ๊ฐ™์€ ๊ฒ€์‚ฌ๋ฅผ ์–ธ์ œ๋“  ๋ฐ˜๋ณตํ•  ์ˆ˜ ์žˆ์–ด, ๋‚˜์ค‘์— ์ฝ”๋“œ๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ ํ™”๋ฉด์ด ๋ง๊ฐ€์ง€์ง€ ์•Š์•˜๋Š”์ง€ ๋น ๋ฅด๊ฒŒ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ์šฉ์–ด (๋น„๊ฐœ๋ฐœ์ž์šฉ)#

์šฉ์–ด์‰ฌ์šด ์„ค๋ช…
BDD"์ด๋ ‡๊ฒŒ ํ–‰๋™ํ•˜๋ฉด ์ด๋ ‡๊ฒŒ ๋˜์–ด์•ผ ํ•œ๋‹ค"๋Š” ์‹์œผ๋กœ, ์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ์‰ฌ์šด ๋ฌธ์žฅ์œผ๋กœ ๋™์ž‘์„ ์ •์˜ํ•˜๊ณ  ๊ฒ€์‚ฌํ•˜๋Š” ๋ฐฉ์‹
.feature ํŒŒ์ผ๊ฒ€์‚ฌํ•  ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ผ์ƒ์–ด์— ๊ฐ€๊น๊ฒŒ ์ ์–ด ๋‘๋Š” ๋Œ€๋ณธ ํŒŒ์ผ
Gherkin๊ทธ ๋Œ€๋ณธ์„ ์“ฐ๋Š” ์ •ํ•ด์ง„ ๋ฌธ์žฅ ํ˜•์‹(Given/When/Then ๊ฐ™์€ ํ‚ค์›Œ๋“œ)
์œ„์ ฏ ํ…Œ์ŠคํŠธ(widget test)์‹ค์ œ ๊ธฐ๊ธฐ ์—†์ด ์•ฑ ํ™”๋ฉด ํ•œ ์กฐ๊ฐ์ด ์ž˜ ๊ทธ๋ ค์ง€๊ณ  ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ์ž๋™ ๊ฒ€์‚ฌ
Given / When / Then๊ฐ๊ฐ "์ด๋Ÿฐ ์ƒํ™ฉ์—์„œ / ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด / ์ด๋ ‡๊ฒŒ ๋˜์–ด์•ผ ํ•œ๋‹ค"๋ฅผ ๋œปํ•˜๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค์˜ ์„ธ ๋‹จ๊ณ„
Mock(๋ชฉ, MockBloc)์ง„์งœ ๋ฐ์ดํ„ฐ ๋Œ€์‹  ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ ํ‰๋‚ด ๋‚ธ ๊ฐ€์งœ ๋ถ€ํ’ˆ. ๊ฒ€์‚ฌ๋ฅผ ๋น ๋ฅด๊ณ  ์•ˆ์ •์ ์œผ๋กœ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
step ํ•จ์ˆ˜์‹œ๋‚˜๋ฆฌ์˜ค์˜ ํ•œ ๋ฌธ์žฅ์„ ์‹ค์ œ๋กœ ์‹คํ–‰ํ•˜๋Š” ๋™์ž‘์œผ๋กœ ์˜ฎ๊ฒจ ๋†“์€ ์ฝ”๋“œ ์กฐ๊ฐ
๊ณจ๋“  ํ…Œ์ŠคํŠธ(golden test)ํ™”๋ฉด์ด ์ •ํ•ด์ง„ ๊ธฐ์ค€ ์ด๋ฏธ์ง€์™€ ๋˜‘๊ฐ™์ด ๋ณด์ด๋Š”์ง€ ๋น„๊ตํ•˜๋Š” ๊ฒ€์‚ฌ
E2E์‚ฌ์šฉ์ž๊ฐ€ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ์“ฐ๋Š” ํ๋ฆ„ ์ „์ฒด๋ฅผ ํ•œ ๋ฒˆ์— ๊ฒ€์‚ฌํ•˜๋Š” ๋ฐฉ์‹

๊ฐœ์š”#

bdd_widget_test ^2.1.3 ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Gherkin .feature ํŒŒ์ผ์—์„œ Flutter ์œ„์ ฏ ํ…Œ์ŠคํŠธ๋ฅผ ์ž๋™ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

์›Œํฌํ”Œ๋กœ์šฐ#

  1. .feature ํŒŒ์ผ ์ž‘์„ฑ โ†’ test/src/bdd/{name}.feature
  2. hooks/hooks.dart ์ƒ์„ฑ โ†’ TestApp + Mock BLoCs
  3. step/ ๋””๋ ‰ํ† ๋ฆฌ์— step ํ•จ์ˆ˜ ๊ตฌํ˜„
  4. melos run build โ†’ _test.dart ์ž๋™ ์ƒ์„ฑ (GENERATED CODE)
  5. melos run test:bdd:select โ†’ ํ…Œ์ŠคํŠธ ์‹คํ–‰

ํ•„์ˆ˜ ์„ค์ •#

build.yaml#

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

pubspec.yaml (dev_dependencies)#

dev_dependencies:
  bdd_widget_test: ^2.1.3
  bloc_test: ^9.2.0
  mocktail: ^1.0.4
  flutter_test:
    sdk: flutter

hooks/hooks.dart ํŒจํ„ด#

import 'package:bloc_test/bloc_test.dart';
import 'package:coui_flutter/coui_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:resources/resources.dart';

export 'package:flutter_test/flutter_test.dart';

bool _initialized = false;

void setUpBddTests() {
  if (_initialized) return;
  _initialized = true;
  // registerFallbackValue for mocktail
}

class TestApp extends StatelessWidget {
  const TestApp({required this.child, super.key});
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return TranslationProvider(
      child: CoUIApp(
        home: Scaffold(body: child),
        theme: AppTheme.light,
      ),
    );
  }
}

// Mock BLoCs
class MockMyBloc extends MockBloc<MyEvent, MyState> implements MyBloc {}

Step ํ•จ์ˆ˜ ํŒจํ„ด#

Given (setup)#

/// Usage: I am on the store page # ์Šคํ† ์–ด ํŽ˜์ด์ง€์— ์žˆ์Šต๋‹ˆ๋‹ค
Future<void> iAmOnTheStorePage(WidgetTester tester) async {
  setUpBddTests();
  final mockBloc = MockStoreBloc();
  when(() => mockBloc.state).thenReturn(StoreState.initial());
  when(() => mockBloc.stream).thenAnswer((_) => const Stream.empty());

  await tester.pumpWidget(
    TestApp(
      child: BlocProvider<StoreBloc>.value(
        value: mockBloc,
        child: const StorePage(),
      ),
    ),
  );
  await tester.pump();
}

When (action)#

/// Usage: I tap the search button # ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ์„ ํƒญํ•ฉ๋‹ˆ๋‹ค
Future<void> iTapTheSearchButton(WidgetTester tester) async {
  await tester.tap(find.byKey(const Key('search_button')));
  await tester.pump();
}

Then (assertion)#

/// Usage: the book list should be displayed # ๋„์„œ ๋ชฉ๋ก์ด ํ‘œ์‹œ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค
Future<void> theBookListShouldBeDisplayed(WidgetTester tester) async {
  expect(find.byType(ListView), findsOneWidget);
}

ํŒŒ๋ผ๋ฏธํ„ฐ ์ „๋‹ฌ#

When I enter { ' test@example.com ' } in the email field

โ†’

Future<void> iEnterInTheEmailField(WidgetTester tester, String param1) async {
  await tester.enterText(find.byKey(const Key('email')), param1);
  await tester.pump();
}

์ƒ์„ธ ์ฐธ์กฐ#

  • .claude/rules/patrol-bdd-conventions.md - Patrol E2E ํ†ตํ•ฉ ๊ทœ์น™
  • .claude/rules/golden-test-guide.md - ๊ณจ๋“  ํ…Œ์ŠคํŠธ ๊ฐ€์ด๋“œ