문서 목차
이 페이지에서목차

/cc-flutter:test — 테스트 코드 작성 도우미

요청한 종류의 테스트 코드를 만듭니다 — UseCase 단위·BLoC·Widget·BDD(`.feature`→Patrol E2E)·Patrol E2E 중 선택하며, 커버리지 80%+ (핵심 UseCase 100%) 기준을 함께 안내합니다.

항목내용
실행 명령/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).

안에서 무슨 일이 벌어지나요#

  1. 요청 해석 — 검사 종류와 대상, 옵션을 읽어 어떤 테스트를 만들지 정합니다.
  2. 기존 패턴 참고 — 이미 있는 테스트 코드와 공식 문서(flutter_test, bloc_signals_test, patrol 등)를 살펴 같은 방식으로 맞춥니다.
  3. 검사 코드 작성 — 정해진 폴더 구조에 맞춰, "준비 → 실행 → 확인" 순서의 테스트 코드를 생성합니다. 진짜 의존성 대신 가짜(Mock)를 끼워 안전하게 검사합니다.
  4. BDD는 Patrol E2E 전용 — .feature 시나리오 하나로 Patrol E2E 검사 파일만 만들어집니다(위젯 검사는 생성되지 않습니다).
  5. 실행 안내 — 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#

ParameterRequiredDescriptionExample
type✅Test Typeunit, widget, bloc, bdd, patrol
target✅Test targetGetUserUseCase, HomeBloc, LoginPage
--feature❌Feature moduleauth, home
--coverage❌Coverage target80, 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 patrol

Core Rules#

Test Structure#

  • Arrange -> Act -> Assert pattern
  • Each test verifies only one behavior
  • Maintain independence between tests

Mocking#

  • Use mocktail package
  • Replace Repository/UseCase with Mock
  • Configure registerFallbackValue when 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-only tags 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#

PhaseMCP ServerPurpose
Pattern analysisContext7flutter_test, bloc_signals_test, patrol docs
Code searchSerenaExisting test pattern reference
E2E executionpatrol_mcpAI agent-driven E2E verification

Examples#

Generate UseCase Test#

/cc-flutter:test unit GetUserUseCase --feature auth

Generate BLoC Test#

/cc-flutter:test bloc HomeBloc --feature home

Generate Widget Test#

/cc-flutter:test widget LoginPage --feature auth

Generate BDD Scenario (Patrol E2E)#

/cc-flutter:test bdd community

Patrol E2E#

/cc-flutter:test patrol auth_flow --feature auth

References#

  • Detailed implementation: agents/test.md
  • BDD generation: commands/bdd/generate.md
  • BDD patterns: rules/bdd-test-patterns.md

검색어를 입력하면 문서를 찾습니다.

↑ ↓ 이동 · Enter 열기Esc 닫기