| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| Invoke | /test |
| Aliases | /test:unit, /test:widget, /test:bloc, /test:patrol |
| Category | kobic-development |
| Complexity | moderate |
| MCP Servers | serena, context7 |
/test โ ํ ์คํธ ์ฝ๋ ์์ฑ ๋์ฐ๋ฏธ#
ํ๋ง๋๋ก#
์๋ก ๋ง๋ ๊ธฐ๋ฅ์ด ์ค์ ๋ก ์ ๋์๊ฐ๋์ง ํ์ธํ๋ "๊ฒ์ฌ ์ฝ๋(ํ ์คํธ)"๋ฅผ ๋์ ์์ฑํด ์ฃผ๋ ๋๊ตฌ์ ๋๋ค. ์ ํ์ ์ถ๊ณ ํ๊ธฐ ์ , ํ์ง ๊ฒ์ฌ ๋ผ์ธ์์ ์๋์ผ๋ก ๋ถ๋์ ๊ฑธ๋ฌ๋ด๋ ๊ฒ์ ์ฅ์น๋ฅผ ๊น์ ์ฃผ๋ ๊ฒ๊ณผ ๊ฐ์์.
๋๊ฐยท์ธ์ ์ฐ๋์#
- ์ ๊ธฐ๋ฅ์ ๋ง๋ ๋ค, ๊ทธ ๊ธฐ๋ฅ์ด ์๋๋๋ก ๋์ํ๋์ง ์๋ ๊ฒ์ฌ ์ฝ๋๋ฅผ ๋ถ์ด๊ณ ์ถ์ ๋
- ๊ฒ์ฌ ๋ฒ์(์ปค๋ฒ๋ฆฌ์ง)๋ฅผ ๋ํ์ ๋น ์ง ๋ถ๋ถ ์์ด ๊ผผ๊ผผํ ํ์ธํ๊ณ ์ถ์ ๋
- ์ฝ๋๋ฅผ ๋จผ์ ๊ฒ์ฌ ๊ธฐ์ค๋ถํฐ ์ ํ๊ณ ๋ง๋๋ ๊ฐ๋ฐ ๋ฐฉ์(TDD/BDD)์ผ๋ก ์ผํ ๋
- ์ฌ์ฉ์๊ฐ ์ค์ ๋ก ํ๋ฉด์ ๋๋ฅด๊ณ ๋์ด๊ฐ๋ ํ๋ฆ๊น์ง ํต์งธ๋ก ์ ๊ฒํ๋ E2E ๊ฒ์ฌ(Patrol)๋ฅผ ๋ง๋ค ๋
๋ฌด์์ ํด์ฃผ๋์#
์์ฒญํ ๊ฒ์ฌ ์ข ๋ฅ์ ๋ง์ถฐ, ๋ฐ๋ก ์ธ ์ ์๋ ๊ฒ์ฌ ์ฝ๋์ ์ค์ ํ์ผ์ ๋ง๋ค์ด ์ค๋๋ค.
- UseCase ๋จ์ ํ ์คํธ โ ๊ธฐ๋ฅ ํ ์กฐ๊ฐ์ด ๋ง๊ฒ ๋์ํ๋์ง ๊ฒ์ฌ
- BLoC ํ ์คํธ โ ํ๋ฉด์ ์ํ(๋ก๋ฉ / ์๋ฃ / ์๋ฌ)๊ฐ ์ ๋๋ก ๋ฐ๋๋์ง ๊ฒ์ฌ
- Widget ํ ์คํธ โ ํ๋ฉด์ด ์ํฉ์ ๋ง๊ฒ ์ฌ๋ฐ๋ก ๊ทธ๋ ค์ง๋์ง ๊ฒ์ฌ
-
BDD ์๋๋ฆฌ์ค โ "์ด๋ฐ ์ํฉ์์ ์ด๋ ๊ฒ ๋์ํด์ผ ํ๋ค"๋ ์๋๋ฆฌ์ค(
.feature) ํ ๊ฐ๋ก ์์ ฏยทPatrol ๊ฒ์ฌ ๋ ์ข ๋ฅ๋ฅผ ๋์์ ์์ฑ - Patrol E2E ํ ์คํธ โ ์ค์ ์ฑ์ ์ผ๊ณ ์ฌ๋์ฒ๋ผ ๋๋ฌ ๋ณด๋ ํตํฉ ๊ฒ์ฌ
๊ฒ์ฌ ๋ชฉํ๋ ์ต์ 80% ์ด์์ ๊ถ์ฅํ๋ฉฐ, ํต์ฌ ๋ก์ง(UseCase)์ 100% ๊ฒ์ฌํ๋๋ก ์๋ดํฉ๋๋ค.
์ด๋ป๊ฒ ์ฐ๋์#
# UseCase ๋จ์ ํ
์คํธ ๋ง๋ค๊ธฐ (auth ๊ธฐ๋ฅ)
/test unit GetUserUseCase --feature auth
# ํ๋ฉด ์ํ(BLoC) ํ
์คํธ ๋ง๋ค๊ธฐ (home ๊ธฐ๋ฅ)
/test bloc HomeBloc --feature home
# ํ๋ฉด(Widget) ํ
์คํธ ๋ง๋ค๊ธฐ (auth ๊ธฐ๋ฅ)
/test widget LoginPage --feature auth
# BDD ์๋๋ฆฌ์ค ๋ง๋ค๊ธฐ (์์ ฏ+Patrol ๋์ ์์ฑ)
/test bdd community
# Patrol E2E ํ
์คํธ ๋ง๋ค๊ธฐ (auth ๊ธฐ๋ฅ)
/test patrol auth_flow --feature auth
-
๋งจ ์์ ๊ฒ์ฌ ์ข
๋ฅ(
unit/widget/bloc/bdd/patrol)์ ๊ฒ์ฌ ๋์์ ์ ์ต๋๋ค. --feature๋ก ์ด๋ค ๊ธฐ๋ฅ ๋ชจ๋์ธ์ง ์ง์ ํ๊ณ ,--coverage 90์ฒ๋ผ ๊ฒ์ฌ ๋ชฉํ ๋น์จ์ ์ ํ ์ ์์ต๋๋ค(๊ธฐ๋ณธ 80).
์์์ ๋ฌด์จ ์ผ์ด ๋ฒ์ด์ง๋์#
- ์์ฒญ ํด์ โ ๊ฒ์ฌ ์ข ๋ฅ์ ๋์, ์ต์ ์ ์ฝ์ด ์ด๋ค ํ ์คํธ๋ฅผ ๋ง๋ค์ง ์ ํฉ๋๋ค.
- ๊ธฐ์กด ํจํด ์ฐธ๊ณ โ ์ด๋ฏธ ์๋ ํ ์คํธ ์ฝ๋์ ๊ณต์ ๋ฌธ์(flutter_test, bloc_test, patrol ๋ฑ)๋ฅผ ์ดํด ๊ฐ์ ๋ฐฉ์์ผ๋ก ๋ง์ถฅ๋๋ค.
- ๊ฒ์ฌ ์ฝ๋ ์์ฑ โ ์ ํด์ง ํด๋ ๊ตฌ์กฐ์ ๋ง์ถฐ, "์ค๋น โ ์คํ โ ํ์ธ" ์์์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค. ์ง์ง ์์กด์ฑ ๋์ ๊ฐ์ง(Mock)๋ฅผ ๋ผ์ ์์ ํ๊ฒ ๊ฒ์ฌํฉ๋๋ค.
- BDD๋ ํ ๋ฒ์ ๋ ๋ฒ โ
.feature์๋๋ฆฌ์ค ํ๋๋ก ์์ ฏ์ฉยทPatrol์ฉ ๊ฒ์ฌ ํ์ผ์ด ํจ๊ป ๋ง๋ค์ด์ง๋๋ค. -
์คํ ์๋ด โ
melos run test,flutter test,patrol test๊ฐ์ ์ค์ ์คํ ๋ช ๋ น์ ํจ๊ป ์๋ ค ์ค๋๋ค.
โ๏ธ ์์ธ ์ต์ ยท์คํ ๋ช ์ธ (๊ฐ๋ฐ์ / AI ์์ด์ ํธ์ฉ)
Triggers#
- When writing new test files
- When improving test coverage
- During TDD/BDD development
- When writing Patrol E2E tests
Context Trigger Pattern#
/test {type} {target} [--options]Parameters#
| Parameter | Required | Description | Example |
|---|---|---|---|
type | โ | Test Type | unit, widget, bloc, bdd, patrol |
target | โ | Test target | GetUserUseCase, HomeBloc, LoginPage |
--feature | โ | Feature module | auth, home |
--coverage | โ | Coverage target | 80, 90 (default: 80) |
Test Structure#
feature/{location}/{feature_name}/test/
โโโ src/
โ โโโ domain/
โ โ โโโ usecase/ # UseCase ๋จ์ ํ
์คํธ
โ โโโ data/
โ โ โโโ repository/ # Repository ํ
์คํธ (mocked)
โ โโโ presentation/
โ โโโ bloc/ # BLoC ํ
์คํธ
โ โโโ widget/ # Widget ํ
์คํธ
โโโ feature/ # BDD ํ
์คํธ
โโโ {feature}.feature
โโโ step/UseCase Unit Test#
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:dependencies/dependencies.dart';
class MockI{Feature}Repository extends Mock implements I{Feature}Repository {}
void main() {
late Get{Entity}UseCase useCase;
late MockI{Feature}Repository mockRepository;
setUp(() {
mockRepository = MockI{Feature}Repository();
useCase = Get{Entity}UseCase(mockRepository);
});
group('Get{Entity}UseCase', () {
final tEntity = {Entity}(id: 1, name: 'Test');
final tParams = Get{Entity}Params(id: 1);
test('should return entity when repository succeeds', () async {
// Arrange
when(() => mockRepository.get{Entity}(any()))
.thenAnswer((_) async => Right(tEntity));
// Act
final result = await useCase(tParams);
// Assert
expect(result, Right(tEntity));
verify(() => mockRepository.get{Entity}(1)).called(1);
verifyNoMoreInteractions(mockRepository);
});
test('should return failure when repository fails', () async {
// Arrange
final tFailure = ServerFailure('Error');
when(() => mockRepository.get{Entity}(any()))
.thenAnswer((_) async => Left(tFailure));
// Act
final result = await useCase(tParams);
// Assert
expect(result, Left(tFailure));
});
});
}
BLoC Test#
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockGet{Entity}UseCase extends Mock implements Get{Entity}UseCase {}
void main() {
late {Feature}Bloc bloc;
late MockGet{Entity}UseCase mockUseCase;
setUp(() {
mockUseCase = MockGet{Entity}UseCase();
bloc = {Feature}Bloc();
});
tearDown(() {
bloc.close();
});
group('{Feature}Bloc', () {
test('initial state is Initial', () {
expect(bloc.state, const {Feature}State.initial());
});
blocTest<{Feature}Bloc, {Feature}State>(
'emits [Loading, Loaded] when load succeeds',
build: () => bloc,
act: (bloc) => bloc.load(),
expect: () => [
const {Feature}State.loading(),
isA<{Feature}State>().having(
(s) => s.maybeMap(loaded: (l) => l.items, orElse: () => null),
'items',
isNotEmpty,
),
],
);
blocTest<{Feature}Bloc, {Feature}State>(
'emits [Loading, Error] when load fails',
build: () => bloc,
act: (bloc) => bloc.load(),
expect: () => [
const {Feature}State.loading(),
isA<{Feature}State>().having(
(s) => s.maybeMap(error: (e) => e.failure, orElse: () => null),
'failure',
isNotNull,
),
],
);
});
}
Widget Test#
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mocktail/mocktail.dart';
class Mock{Feature}Bloc extends MockBloc<{Feature}Event, {Feature}State>
implements {Feature}Bloc {}
void main() {
late Mock{Feature}Bloc mockBloc;
setUp(() {
mockBloc = Mock{Feature}Bloc();
});
Widget buildTestWidget() {
return MaterialApp(
home: BlocProvider<{Feature}Bloc>.value(
value: mockBloc,
child: const {Feature}Page(),
),
);
}
group('{Feature}Page', () {
testWidgets('renders loading indicator when loading', (tester) async {
// Arrange
when(() => mockBloc.state).thenReturn(const {Feature}State.loading());
// Act
await tester.pumpWidget(buildTestWidget());
// Assert
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('renders list when loaded', (tester) async {
// Arrange
final items = [
{Entity}(id: 1, name: 'Item 1'),
{Entity}(id: 2, name: 'Item 2'),
];
when(() => mockBloc.state).thenReturn(
{Feature}State.loaded(items: items),
);
// Act
await tester.pumpWidget(buildTestWidget());
// Assert
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 2'), findsOneWidget);
});
testWidgets('shows error message when error', (tester) async {
// Arrange
when(() => mockBloc.state).thenReturn(
{Feature}State.error(ServerFailure('Network error')),
);
// Act
await tester.pumpWidget(buildTestWidget());
// Assert
expect(find.text('Network error'), findsOneWidget);
});
});
}
BDD Test (TestDriver Pattern โ Recommended)#
.feature File#
@smoke
@auth
Feature: Login Page # ๋ก๊ทธ์ธ ํ์ด์ง
Background:
Given I am on the login page # ๋ก๊ทธ์ธ ํ์ด์ง
@validation
Scenario: Email field validation # ์ด๋ฉ์ผ ์ ํจ์ฑ ๊ฒ์ฌ
When I enter { ' invalid-email ' } in the email field # ์๋ชป๋ ์ด๋ฉ์ผ
Then the email error should be displayed # ์๋ฌ ํ์
@patrol-only
Scenario: Native back button # ๋ค์ดํฐ๋ธ ๋ฐฑ ๋ฒํผ
When I press the back button # ๋ฐฑ ๋ฒํผ
Then the app should navigate back # ์ด์ ํ๋ฉด ๋ณต๊ทStep Function (TestDriver-based)#
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);
}
build.yaml (Dual test generation)#
targets:
$default:
builders:
bdd_test_gen|dual_test_gen:
enabled: true
generate_for:
- test/src/bdd/*.feature
options:
stepFolder: step
.featureโ.widget_test.dart+.patrol_test.dartgenerated simultaneously
BDD Test (Legacy bdd_widget_test)#
Feature: {Feature} List View # {Feature} ๋ชฉ๋ก ์กฐํ
User can view {Feature} list.
Background:
Given the app is running # ์ฑ์ด ์คํ๋์ด ์๋ค
Scenario: List loads successfully # ๋ชฉ๋ก ์กฐํ ์ฑ๊ณต
When user navigates to {feature} screen
Then {entity} list is displayedPatrol E2E Test#
import 'package:patrol/patrol.dart';
import 'package:test_driver/test_driver.dart';
void main() {
patrolTest(
'Given logged in, When store, Then book list',
config: const PatrolTesterConfig(settleTimeout: Duration(seconds: 15)),
($) async {
final driver = PatrolTestDriver($);
await driver.tap(K.navStore);
await driver.expectVisible(K.bookList);
},
);
}
Test Commands#
# ์ ์ฒด ํ
์คํธ
melos run test
# Feature๋ณ ํ
์คํธ
melos run test --scope=feature_{feature_name}
# ์ปค๋ฒ๋ฆฌ์ง ํฌํจ
melos run test:with-html-coverage
# BDD tests
melos run test:bdd
melos run test:bdd:select
# Code generation (BDD โ _test.dart)
melos run build
# Patrol E2E
patrol test --target integration_test/scenarios/smoke_test.dart
# Tag filtering
flutter test --tags smoke
flutter test --exclude-tags patrol-onlyCore Rules#
Test Structure#
- Arrange -> Act -> Assert pattern
- Each test verifies only one behavior
- Maintain independence between tests
Mocking#
- Use
mocktailpackage - Replace Repository/UseCase with Mock
- Configure
registerFallbackValuewhen needed
BDD Tests#
- Follow Gherkin syntax
- Step functions should use TestDriver (recommended) or WidgetTester
- K class for centralized widget Key management
@widget-only,@patrol-onlytags for test target control
Coverage#
- Target minimum 80% code coverage
- UseCase 100% testing required
- BLoC main flow testing required
MCP Integration#
| Phase | MCP Server | Purpose |
|---|---|---|
| Pattern analysis | Context7 | flutter_test, bloc_test, patrol docs |
| Code search | Serena | Existing test pattern reference |
| E2E execution | patrol_mcp | AI agent-driven E2E verification |
Examples#
Generate UseCase Test#
/test unit GetUserUseCase --feature authGenerate BLoC Test#
/test bloc HomeBloc --feature homeGenerate Widget Test#
/test widget LoginPage --feature authGenerate BDD Scenario (Dual Test)#
/test bdd communityPatrol E2E#
/test patrol auth_flow --feature authReferences#
- Detailed implementation:
agents/test.md - BDD generation:
commands/bdd/generate.md - BDD patterns:
rules/bdd-test-patterns.md