Feature Test Bundle — unit + BDD(Patrol E2E) + E2E 3-layer 동시 스캐폴드
<!-- humanized:non-dev -->
명시적 수기 Patrol legacy 전용 참고. 저장소가 이 방식을 선언한 경우에만 아래의 Patrol 배치·리셋·step 템플릿을 사용한다. co_bdd feature-local BDD에는 적용하지 않는다. unit/widget/golden/Widgetbook/BDD의 전체 완료 계약은 feature-verification을 우선 적용한다. 아래 3-layer 설명은 legacy 실행 구조이며 필수 5종 검증의 면제가 아니다.
Feature Test Bundle — unit + BDD(Patrol E2E) + E2E 3-layer 동시 스캐폴드#
한마디로#
새 기능을 만들면 "이게 제대로 작동하는지 확인하는 검사 도구(테스트)"도 같이 있어야 합니다. 이 스킬은 그 검사 도구 3종 세트를 명령어 한 번으로 한꺼번에 준비해 주는 자동 조립 라인입니다. 마치 새 가전제품을 출고하기 전, 부품 검사·기능 검사·실사용 검사를 한 번에 세팅해 주는 품질 관리 라인과 같습니다. 검사의 '틀'을 만들어 주고, 세부 내용은 사람이나 다른 도구가 채워 넣습니다.
무엇을·언제#
-
무엇을 해주나요
- 작은 부품 단위 검사(Unit), 사용자 시나리오 검사(BDD), 여러 화면을 넘나드는 실제 흐름 검사(E2E) 세 가지 검사 틀을 한 번에 만들어 줍니다.
- 검사 틀만 자동으로 만들고, 비워 둔 부분(채워야 할 곳)을 목록으로 알려 줍니다.
- 만든 직후 자동으로 한 번 검사를 돌려서, 빠진 부분이 없는지 미리 점검합니다.
-
언제 작동하나요
/cc-e2e:kick-off {기능이름}명령을 실행했을 때- 새 기능 구현을 막 끝내고 검사 초안을 빠르게 갖추고 싶을 때
- 코드 제출(PR) 전에 "검사 3종이 모두 걸려 있는지" 확인하고 싶을 때
핵심 용어#
| 용어 | 쉬운 설명 |
|---|---|
| 테스트(test) | 코드가 의도대로 작동하는지 자동으로 검사하는 절차 |
| Unit(단위) 테스트 | 가장 작은 부품 하나하나가 제대로 도는지 보는 검사 |
| BDD | "이렇게 하면 이렇게 된다"는 사용자 행동 문장으로 적는 시나리오 검사 방식 |
| E2E | 시작부터 끝까지, 여러 화면을 넘나드는 실제 사용 흐름을 통째로 보는 검사 |
| 스캐폴드(scaffold) | 내용은 비어 있지만 뼈대만 갖춘 틀(시작용 양식) |
| feature(기능) | 앱에서 하나의 기능 묶음(예: 로그인, 검색) |
.feature 파일 | 사용자 시나리오를 사람이 읽기 쉬운 문장으로 적어 둔 파일 |
| step(스텝) | 시나리오 문장 하나를 실제로 실행하는 작은 동작 정의 |
| Patrol | 실제 앱을 켜서 사람처럼 눌러 보며 검사하는 도구 |
| widget(위젯) | Flutter 앱 화면을 이루는 UI 조각 |
| mock(목) | 진짜 대신 끼워 넣는 가짜 부품(검사용 대역) |
| 로컬 풀스택 | 내 컴퓨터에서 서버·DB·앱을 한꺼번에 띄워 통째로 돌려 보는 환경 |
| PR | 작성한 코드를 합치자고 올리는 변경 요청(Pull Request) |
| TODO | "여기는 나중에 채워야 함"이라고 표시해 둔 빈칸 |
| 오케스트레이션 | 여러 도구·단계를 순서대로 묶어 지휘하는 일 |
목적#
기능 구현 완료 직후 다음을 하나의 명령 으로 준비:
- Unit 레이어 — UseCase, Repository, BLoC 단위 테스트 (Mocktail + bloc_test)
-
BDD 레이어(Patrol E2E 전용, 손으로 작성) —
.feature+ TestDriver step + Patrol 테스트를 전부app/{app}/integration_test/에 마련합니다. 코드 생성기를 쓰지 않고, 위젯 테스트도 만들지 않습니다 — BDD 시나리오의 유일한 실행 경로는 손으로 쓴 Patrol E2E 입니다. feature 패키지 안에는 BDD 관련 파일이 하나도 남지 않습니다. -
비-BDD 위젯 레이어(선택, Gherkin 과 무관) — 필요하면 feature 패키지 안에 통상적
testWidgets위젯 테스트를 손으로 스캐폴드합니다. 이건 BDD.feature에서 파생되지 않고,cc-flutter:widget-testing이 다루는 완전히 별개의 테스트입니다.
각 레이어는 기존 조직 스킬에 위임 — 본 스킬은 오케스트레이션만 담당합니다.
트리거#
/cc-e2e:kick-off {feature}실행- 기능 구현 직후 테스트 초안을 빠르게 갖추고 싶을 때
- PR 제출 전에 "테스트 3 레이어가 모두 걸려 있는지" 확인할 때
전제#
-
feature 역할 패키지 이미 존재 (
feature/{name}/{app,console,server,client}/) — 없으면cc-bricks또는cc-flutter:feature:*로 먼저 scaffold - 해당 feature 의 도메인/데이터/프레젠테이션 3 레이어 코드가 최소 작성됨
-
package/test_driver의존성이 앱(app/{app}/pubspec.yaml)에 반영됨. ⛔bdd_test_gen/co_test_gen은 더 이상 의존성으로 필요하지 않습니다 — 코드 생성기를 쓰지 않습니다. - (선택) flutter-skill MCP 사용 가능 — 탐색 단계 수행 시에만 필요
동작#
0. 배치 판정과 모듈/호스트 경계 (AD-2)#
먼저 .cob/feature-layout.json을 읽고, 없으면 cob의 기존 legacy 트리 판정 → 나머지는 bundle을 따른다. marker를 보존하고
생성 배치 계약을 적용한다. legacy 안내는 해당 판정인 프로젝트에만 적용한다.
| 검사 대상 | 소유 위치 | 실행 |
|---|---|---|
| UI UseCase/BLoC/Repository·일반 위젯·골든 | feature/{name}/{app,console}/test/ |
각 <name>·console_<name> 패키지의 flutter test |
| 순수 서버 서비스·정책·검증 (Session/DB 없음) | feature/{name}/server/test/unit/ |
<name>_server 모듈의 dart test test/unit/ |
| Session/DB/withServerpod·dispatcher·HTTP/SDK·교차 기능 | backend/{project}_server/test/integration/ |
실행 호스트의 통합 러너 |
| Patrol BDD/E2E | app/{app}/integration_test/ | 실제 실행 앱의 Patrol 러너 |
bundle.json은 조립 선언이고 hostTests에 호스트 검증을 등재한다. host-integration.json은 포트·식별자·이름 등의 결합 계약이다. 호스트가
<Feature>HostBindings.bind(serverpod, …) / forSession(session)으로 세션 인지 포트를 연결한다. 바인딩 부재·미구현 어댑터가 fail-closed로 실패하는 검증도 호스트에 둔다. 통합 실행은 프로젝트의 격리 DB·세션별 포트/base-URL 계약을 사용하며 순수 모듈 테스트가 DB를 띄우지 않는다. 신규 기본 공식
4.0.3, donor 순수 테스트는 TZ=UTC에서도 실행한다.
1. 사전 조사#
-
feature/{name}/{app,console}/lib/를 읽어 UseCase, BLoC, Repository 목록 수집; 서버 순수 코드와 호스트 결합도 확인 -
feature/{name}/{app,console}/test/·server/test/unit/·호스트test/integration/의 기존 테스트 조사 — 중복 생성 방지 -
app/{app}/integration_test/features/{name}.feature유무 판단 (⛔ feature 패키지 안이 아니라 항상 app 레벨을 본다) .claude/e2e-lock.json에서 해당 feature 의 lock 된 시나리오 파악 (재저작 방지)
2. Unit 레이어 스캐폴드 (cc-flutter:unit-testing 위임)#
각 UseCase / BLoC / Repository 에 대해 테스트 파일 생성:
-
UseCase:
test/src/domain/usecase/{use_case_name}_test.dart- Mocktail 로 Repository mock
should return entity when repository succeeds/should return failure when repository fails기본 2 테스트
-
BLoC:
test/src/presentation/bloc/{bloc_name}_test.dartbloc_test의blocTest()사용emits [Loading, Loaded]/emits [Loading, Error]기본 시나리오
-
Repository:
test/src/data/repository/{repository_name}_test.dart- DataSource mock
- 주요 메서드별 성공/실패 케이스
실제 스캐폴드는 cc-flutter/commands/test.md 의 /cc-flutter:test unit {target} 템플릿을 호출하여 일관성 유지.
3. BDD 레이어 (app/{app}/integration_test/, 손으로 작성 — 코드 생성기 없음)#
⛔ feature 패키지 안에는 아무것도 만들지 않습니다. .feature·step·Patrol 테스트 전부
app/{app}/integration_test/ 하나에 모입니다. 예전엔 "cross-feature 일 때만" 이관했지만,
이제는 애초에 그쪽에만 생깁니다 — 이관이라는 개념 자체가 없습니다.
3a. .feature 파일 확보
flag { app/{app}/integration_test/features/{name}.feature 존재 여부 }
├── 존재 → 그대로 사용 (경고 주석 제거 상태여야 함)
└── 없음
├── flutter-skill 세션 실행 가능 → /cc-e2e:explore → /cc-e2e:draft 연속 호출 제안
└── 아니면 → 최소 skeleton 생성 (영어 + 한글 주석 포맷)
Skeleton 예시 (app/{app}/integration_test/features/{name}.feature):
# DRAFT — auto-generated skeleton by feature-test-bundle
# generated: {timestamp}
# ⚠️ Replace placeholder steps with actual scenarios before /cc-e2e:lock.
@smoke
@{feature_domain}
Feature: {Feature PascalCase} # {한글 이름}
{one-line user value} # {한글 번역}
Background:
Given I am on the {feature} page # {feature} 페이지에 있다
Scenario: Happy path for {feature} # {feature} 정상 흐름
When I tap the {primary_action} button # {primary_action} 버튼 탭
Then the {expected_state} should be displayed # {expected_state} 표시
3b. TestDriver step 파일 스캐폴드
.feature 의 Given/When/Then 을 파싱해 각 문장에 대응하는 step 파일 템플릿을
app/{app}/integration_test/step/에 생성:
- 파일명: snake_case 영어 (
i_tap_the_sign_in_button.dart) - 함수명: camelCase (
iTapTheSignInButton) -
본문은
TestDriver인자 +TODO주석 (사람이 채움). 이미bdd-canonical-steps에 있는 표준 표현이면 로컬 step 을 새로 만들지 말고package:test_driver/shared_steps.dart를 import 해서 그대로 씁니다. -
필요한 K 상수가
package/test_driver/lib/src/key/k.dart에 없으면key-harden-targets.md에 추가 등록
3c. Patrol 시나리오 파일 스캐폴드 — 배치가 기본값
.feature 의 Scenario 를 Background 기준으로 묶어 파일을 배분합니다
(rules/feature-file-conventions.md §5.1):
같은 Background 를 공유하는 Scenario 수 · @isolated 여부
├── 3건 이상 & @isolated 아님 → scenarios/{name}_{screen}_batch_test.dart (배치, 기본)
└── 그 외 (@isolated · 2건 이하) → scenarios/{name}_{scenario}_test.dart (1파일 1시나리오)
배치 템플릿 (코드 생성기 없음 — 이 스킬이 틀을 만들고 TODO 만 사람이 채웁니다):
void main() {
patrolTest(
'''{Screen} — batch ({N} scenarios)''',
config: patrolConfig,
($) async {
final driver = PatrolTestDriver($);
// Background — 배치당 1회. .feature 의 Background step 순서대로
await theAppIsInitialized($);
// TODO: .feature 의 Background 를 여기 채운다 (도달까지만)
await runScreenBatch(
$,
screen: '{screen_slug}',
reset: () => the{Screen}ScreenIsReset($, driver),
scenarios: {
'{Scenario 이름 — .feature 그대로}': () async {
// TODO: 이 Scenario 의 When/Then step 호출을 채운다
},
// ... 나머지 Scenario
},
);
},
);
}
단독 템플릿 (@isolated · 2건 이하):
void main() {
patrolTest(
'''{Scenario 이름 — .feature 그대로}''',
config: patrolConfig,
($) async {
final driver = PatrolTestDriver($);
await theAppIsInitialized($);
// TODO: .feature 의 Given/When/Then 순서대로 step 호출을 채운다
},
);
}
배치 파일을 만들 때는 화면 리셋 step 스텁도 함께 생성합니다 —
step/the_{screen}_screen_is_reset.dart 에 3항 의무(닫기 · 되돌리기 · 앵커 단정)를 TODO 로
남기고, 앵커 Key 는 Background 마지막 Then 의 Key 를 기본 제안값으로 씁니다.
helpers/screen_batch.dart 가 없으면 러너 스텁도 함께 만듭니다.
test_bundle.dart 에 import 추가, run_all_scenarios.sh 갱신.
배치 단위·리셋 계약·실패/BLOCKED 의미론의 정본은
cc-flutter/skills/patrol-bdd-conventions의 "화면 단위 배치" 절입니다.
백엔드 정책 — Staging 을 먼저 고려, 로컬 풀스택은 조건부.
app/{app}/integration_test/scenarios/의 Patrol 테스트가 실 서버와 통신해야 한다면, 프로젝트가 Serverpod 4+ 로 새로 시작한 것이 아니면 Staging을 기본으로 검토한다 — 앱의 기존 client/OpenApiService/flavor 폴백 설정을 따른다(base-URL 하드코딩 금지). 로컬 풀스택(serverpod start한 명령으로 백엔드 서버 + 내장 Postgres + Flutter 앱을 함께 기동,http://localhost:8080/)은 Serverpod 4+ 신규 프로젝트에 최적화된 기능이며, 기존 Serverpod 3.x 프로젝트에서 흉내 내려면 비공식 서드파티 포크가 필요해 리스크가 있다 — 판단 기준은patrol-bdd-conventions의 "백엔드 대상" 절 참조. unit 레이어(+ BDD 와 무관한 일반 위젯 테스트)의 mock 은 그대로 유지. 이 노트는 Patrol· integration 레이어에만 적용된다. 로컬 풀스택을 쓰기로 했다면 셋업·MCP 연동은serverpod-local-fullstack,serverpod-mcp-guide스킬 참조. (3.x / legacy: 로컬 DB 는docker compose up -d로 기동, 통합테스트는withServerpod+dart test -t integration— API 동일.)
4. 검증 (/cc-dev:pr:preflight 위임)#
스캐폴드 생성 직후 cc-dev:pr:preflight --scope={feature-packages} 를 자동 호출:
| 레이어 | /cc-dev:pr:preflight 실행 명령 | 책임 |
|---|---|---|
| Unit | melos exec --scope=... -- flutter test --exclude-tags=integration |
/cc-dev:pr:preflight |
| Widget (BDD 와 무관한 일반 위젯 테스트만) | 위 명령에 포함 | /cc-dev:pr:preflight |
| Integration @smoke (조건부, BDD 시나리오 포함) | patrol test --target=@smoke (디바이스 있을 때) |
/cc-dev:pr:preflight |
각 레이어 실패 원인은 preflight 리포트에 그룹화해 제시.
/cc-dev:pr:preflight 의 정책은 cc-dev/rules/pr-preflight-policy.md
참조.
5. 리포트#
사용자에게 출력:
- 생성된 테스트 파일 수 (레이어별)
TODO주석이 남은 step 파일 목록 (사람이 채워야 함)-
다음 단계 제안:
TODO해결 →/cc-e2e:explore로 실제 조작 확인 →/cc-e2e:draft→/cc-e2e:lock- 또는 빠른 경로:
cc-flutter:test에이전트로 unit/widget 완성
출력#
Unit 레이어#
feature/{name}/{app,console}/test/src/domain/usecase/*_test.dartfeature/{name}/{app,console}/test/src/presentation/bloc/*_test.dartfeature/{name}/{app,console}/test/src/data/repository/*_test.dartfeature/{name}/server/test/unit/*_test.dart(순수 서버 테스트)
호스트 DB 통합 레이어#
backend/{project}_server/test/integration/{name}_endpoint_test.dartbackend/{project}_server/test/integration/{name}_sdk_test.dartbundle.json.hostTests등재; 세션 인지 포트·미구현 fail-closed·실제 DB/SDK 계약 검증
BDD 레이어 (Patrol E2E 전용, app/{app}/integration_test/ 하나에만 생성)#
app/{app}/integration_test/features/{name}.feature(신규 or 기존 — 정본, 빌드 대상 아님)app/{app}/integration_test/step/*.dart— TestDriver step 템플릿-
app/{app}/integration_test/step/the_{screen}_screen_is_reset.dart— 배치용 리셋 스텁 (배치 1개당 1개) app/{app}/integration_test/helpers/screen_batch.dart— 러너 스텁 (없을 때만)app/{app}/integration_test/scenarios/{name}_{screen}_batch_test.dart— 배치 템플릿 (기본)-
app/{app}/integration_test/scenarios/{name}_{scenario}_test.dart— 단독 템플릿 (@isolated·2건 이하) test_bundle.dart,run_all_scenarios.sh업데이트
위임 매트릭스#
| 레이어 | 위임 대상 | 호출 방식 |
|---|---|---|
| Unit - UseCase | cc-flutter:unit-testing |
/cc-flutter:test unit {UseCase} --feature {name} |
| Unit - BLoC | cc-flutter:unit-testing + cc-flutter:bloc |
/cc-flutter:test bloc {Bloc} --feature {name} |
| Unit - Repository | cc-flutter:unit-testing |
/cc-flutter:test unit {Repository} --feature {name} |
| Widget 개별 (BDD 와 무관, 손으로 작성) | cc-flutter:widget-testing |
/cc-flutter:test widget {Widget} --feature {name} |
| BDD (Patrol E2E 전용, 손으로 작성) | cc-flutter:bdd-testing |
본 스킬이 스캐폴드만, 코드 생성기 없음 |
| Patrol step 구현 | cc-e2e:step-implement + patrol_mcp |
후속 단계 |
| 공유 step 추출 | cc-flutter:bdd-step-reuse-agent |
2+ feature 에서 반복되는 step |
| 검증(Unit+Widget+Integration@smoke) | cc-dev:pr:preflight |
스캐폴드 직후 자동 호출, 실패 시 PR 차단 |
금지 사항#
- ❌ 기존 조직 스킬(
cc-flutter:*) 의 템플릿을 재정의 -
❌ feature 패키지 안에
.feature/test/src/bdd//builder 블록을 두는 것 — BDD 는 항상app/{app}/integration_test/ - ❌ BDD
.feature로부터 위젯 테스트를 만드는 것 (코드 생성이든 수동이든) - ❌ 배치 파일을 만들면서 리셋 step 스텁을 빠뜨리는 것 — 리셋 없는 배치는 상태 누수를 방치한다
- ❌ 같은 시나리오를 배치 파일과 단독 파일에 양쪽 다 스캐폴드하는 것 (두 번 돌게 된다)
- ❌
.feature를 Korean-only 로 생성 (영어 + 한글 주석 필수) - ❌ flutter-skill MCP 를 본 스킬 내에서 직접 호출 —
/cc-e2e:explore커맨드를 통해서만 - ❌ lock 된 feature 의 기존 시나리오 덮어쓰기 — draft 로 별도 생성
실패 케이스#
| 원인 | 대응 |
|---|---|
| feature 모듈 없음 | cc-bricks / cc-flutter:feature:* 안내 |
scenarios/*_test.dart 컴파일 실패 (step 이름 불일치) |
step 파일명/함수명 매핑 재검토,
dart analyze
— 코드 생성기가 없으므로 이 실수는 빌드가 아니라
dart analyze
/리뷰에서 잡아야 한다
|
feature 패키지 안에 레거시 test/src/bdd/ 스캐폴드 발견 |
rules/bdd-test-patterns.md
마이그레이션 절차대로 제거 — Patrol
scenarios/
와 손으로 쓴 위젯 테스트로 분리 이관
|
| K 상수 누락 | key-hardening 재실행 안내 |
.feature skeleton 만으로는 탐색 부족 | /cc-e2e:explore 로 실제 앱 탐색 권유 |
참고#
-
조직 테스트 원전:
cc-flutter/skills/unit-testing/SKILL.mdcc-flutter/skills/widget-testing/SKILL.mdcc-flutter/skills/bdd-testing/SKILL.mdcc-flutter/skills/integration-testing/SKILL.md
- 패턴 룰:
cc-flutter/rules/bdd-test-patterns.md - 공유 step:
cc-flutter/agents/bdd-step-reuse-agent.md -
Patrol/E2E 백엔드 정책(Staging 우선, 로컬 풀스택 조건부):
patrol-bdd-conventions,serverpod-local-fullstack,serverpod-mcp-guide -
본 플러그인 연계:
skills/exploration,skills/scenario-draft,skills/key-hardening,skills/step-implement,skills/scenario-lock