/cc-flutter:test — 테스트 코드 작성 도우미#
| 항목 | 내용 |
|---|---|
| 실행 명령 | /cc-flutter:test |
| 분류 | 개발 |
| 난이도 | ●●○ 보통 |
| MCP 서버 | serena, context7 |
한마디로#
새로 만든 기능이 실제로 잘 돌아가는지 확인하는 "검사 코드(테스트)"를 대신 작성해 주는 도구입니다. 제품을 출고하기 전, 품질 검사 라인에서 자동으로 불량을 걸러내는 검수 장치를 깔아 주는 것과 같아요.
누가·언제 쓰나요#
- 새 기능을 만든 뒤, 그 기능이 의도대로 동작하는지 자동 검사 코드를 붙이고 싶을 때
- 검사 범위(커버리지)를 넓혀서 빠진 부분 없이 꼼꼼히 확인하고 싶을 때
- 코드를 먼저 검사 기준부터 정하고 만드는 개발 방식(TDD/BDD)으로 일할 때
- 사용자가 실제로 화면을 누르고 넘어가는 흐름까지 통째로 점검하는 E2E 검사(Patrol)를 만들 때
무엇을 해주나요#
요청한 검사 종류에 맞춰, 바로 쓸 수 있는 검사 코드와 설정 파일을 만들어 줍니다.
- UseCase 단위 테스트 — 기능 한 조각이 맞게 동작하는지 검사
- BLoC 테스트 — 화면의 상태(로딩 / 완료 / 에러)가 제대로 바뀌는지 검사
- Widget 테스트 — 화면이 상황에 맞게 올바로 그려지는지 검사
-
BDD 시나리오 — "이런 상황에서 이렇게 동작해야 한다"는 시나리오(
.feature)로 Patrol E2E 검사를 생성 (위젯 검사는 만들지 않습니다 — 위젯 검사가 필요하면 별도의 일반 Widget 테스트로 작성하세요) - Patrol E2E 테스트 — 실제 앱을 켜고 사람처럼 눌러 보는 통합 검사
검사 목표는 최소 80% 이상을 권장하며, 핵심 로직(UseCase)은 100% 검사하도록 안내합니다.
어떻게 쓰나요#
# UseCase 단위 테스트 만들기 (auth 기능)
/cc-flutter:test unit GetUserUseCase --feature auth
# 화면 상태(BLoC) 테스트 만들기 (home 기능)
/cc-flutter:test bloc HomeBloc --feature home
# 화면(Widget) 테스트 만들기 (auth 기능)
/cc-flutter:test widget LoginPage --feature auth
# BDD 시나리오 만들기 (Patrol E2E 생성 — 위젯 검사는 생성되지 않음)
/cc-flutter:test bdd community
# Patrol E2E 테스트 만들기 (auth 기능)
/cc-flutter:test patrol auth_flow --feature auth
-
맨 앞에 검사 종류(
unit/widget/bloc/bdd/patrol)와 검사 대상을 적습니다. --feature로 어떤 기능 모듈인지 지정하고,--coverage 90처럼 검사 목표 비율을 정할 수 있습니다(기본 80).
안에서 무슨 일이 벌어지나요#
- 요청 해석 — 검사 종류와 대상, 옵션을 읽어 어떤 테스트를 만들지 정합니다.
- 기존 패턴 참고 — 이미 있는 테스트 코드와 공식 문서(flutter_test, bloc_signals_test, patrol 등)를 살펴 같은 방식으로 맞춥니다.
- 검사 코드 작성 — 정해진 폴더 구조에 맞춰, "준비 → 실행 → 확인" 순서의 테스트 코드를 생성합니다. 진짜 의존성 대신 가짜(Mock)를 끼워 안전하게 검사합니다.
-
BDD는 Patrol E2E 전용 —
.feature시나리오 하나로 Patrol E2E 검사 파일만 만들어집니다(위젯 검사는 생성되지 않습니다). -
실행 안내 —
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#
/cc-flutter: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·step·Patrol 시나리오는 전부
app/{app}/integration_test/에 있습니다:
app/{app}/integration_test/
├── features/{feature}.feature
├── step/
└── scenarios/
├── {feature}_{screen}_batch_test.dart # 기본: 같은 Background 시나리오 3건+ 를 한 세션에
└── {feature}_{scenario}_test.dart # @isolated 또는 2건 이하일 때만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_signals_test/bloc_signals_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.stateValue, const {Feature}State.initial());
});
blocSignalTest<{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,
),
],
);
blocSignalTest<{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:bloc_signals_flutter/bloc_signals_flutter.dart';
import 'package:mocktail/mocktail.dart';
// bloc_signals 에는 MockBloc/whenListen 이 없다. state 는 ReadonlySignal 이라
// 목으로 값을 돌려줘도 리빌드가 일어나지 않는다 → 실제 컨테이너를 시드해서 쓴다.
void main() {
late MockGet{Entity}UseCase mockUseCase;
setUp(() {
mockUseCase = MockGet{Entity}UseCase();
});
Widget buildTestWidget({Feature}State initialState) {
final bloc = {Feature}Bloc(mockUseCase, initialState: initialState);
addTearDown(bloc.close);
return MaterialApp(
home: BlocSignalProvider<{Feature}Bloc>.value(
value: bloc,
child: const {Feature}Page(),
),
);
}
group('{Feature}Page', () {
testWidgets('renders loading indicator when loading', (tester) async {
// Arrange
// Act
await tester.pumpWidget(buildTestWidget(const {Feature}State.loading()));
// 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'),
];
// Act
await tester.pumpWidget(
buildTestWidget({Feature}State.loaded(items: items)),
);
// Assert
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 2'), findsOneWidget);
});
testWidgets('shows error message when error', (tester) async {
// Arrange
// Act
await tester.pumpWidget(
buildTestWidget({Feature}State.error(ServerFailure('Network error'))),
);
// Assert
expect(find.text('Network error'), findsOneWidget);
});
});
}
BDD Test (TestDriver Pattern — only supported form, hand-written as Patrol E2E only)#
.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 # 에러 표시
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);
}
Hand-written, no build.yaml#
⛔ No code generator runs in this pipeline — not bdd_test_gen, not co_test_gen
(dual_test_gen), not bdd_widget_test. There is no build.yaml builder block to configure for
BDD. .feature is a spec document under app/{app}/integration_test/features/; the Patrol test
under app/{app}/integration_test/scenarios/ is written by hand, copying the .feature Scenario
name and step order directly. Scenarios sharing one Background (3 or more) go into a single
screen batch file ({name}_{screen}_batch_test.dart) so the arrival cost is paid once; scenarios
tagged @isolated stay one-per-file ({name}_{scenario}_test.dart). See
cc-flutter/skills/patrol-bdd-conventions → "화면 단위 배치". Shared steps are imported directly:
import 'package:test_driver/shared_steps.dart';
⛔ Legacy bdd_widget_test is retired. BDD scenarios no longer generate widget tests at all —
there is no widget-targeted variant of this pattern to fall back to. A fast, mock-backed,
non-Gherkin check belongs to plain Widget testing instead (see skills/widget-testing).
Patrol 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
# ⛔ melos run test:bdd / test:bdd:select / melos run build (BDD → _test.dart) are retired —
# there is no BDD codegen pipeline. Patrol E2E is the only way to run a BDD scenario:
patrol test --target integration_test/scenarios/smoke_test.dart
# Tag filtering
flutter test --tags smoke
flutter test --exclude-tags patrolCore 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 must use TestDriver (⛔
WidgetTester-based step functions are retired for BDD) - K class for centralized widget Key management
- ⛔
@both/@widget-only/@patrol-onlytags are retired — every scenario is Patrol E2E only, so there is no widget/Patrol target to select. Grading/scheduling tags (@smoke,@P0–@P2, domain tags) are unaffected and still apply.
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_signals_test, patrol docs |
| Code search | Serena | Existing test pattern reference |
| E2E execution | patrol_mcp | AI agent-driven E2E verification |
Examples#
Generate UseCase Test#
/cc-flutter:test unit GetUserUseCase --feature authGenerate BLoC Test#
/cc-flutter:test bloc HomeBloc --feature homeGenerate Widget Test#
/cc-flutter:test widget LoginPage --feature authGenerate BDD Scenario (Patrol E2E)#
/cc-flutter:test bdd communityPatrol E2E#
/cc-flutter:test patrol auth_flow --feature authReferences#
- Detailed implementation:
agents/test.md - BDD generation:
commands/bdd/generate.md - BDD patterns:
rules/bdd-test-patterns.md