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 ์์ ฏ ํ
์คํธ๋ฅผ ์๋ ์์ฑํฉ๋๋ค.
์ํฌํ๋ก์ฐ#
.featureํ์ผ ์์ฑ โtest/src/bdd/{name}.featurehooks/hooks.dart์์ฑ โ TestApp + Mock BLoCsstep/๋๋ ํ ๋ฆฌ์ step ํจ์ ๊ตฌํmelos run buildโ_test.dart์๋ ์์ฑ (GENERATED CODE)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- ๊ณจ๋ ํ ์คํธ ๊ฐ์ด๋