LogoSkills

test

Unit/Widget/BDD/Patrol test writing guide

ํ•ญ๋ชฉ๋‚ด์šฉ
Invoke/test
Aliases/test:unit, /test:widget, /test:bloc, /test:patrol
Categorykobic-development
Complexitymoderate
MCP Serversserena, 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).

์•ˆ์—์„œ ๋ฌด์Šจ ์ผ์ด ๋ฒŒ์–ด์ง€๋‚˜์š”#

  1. ์š”์ฒญ ํ•ด์„ โ€” ๊ฒ€์‚ฌ ์ข…๋ฅ˜์™€ ๋Œ€์ƒ, ์˜ต์…˜์„ ์ฝ์–ด ์–ด๋–ค ํ…Œ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค์ง€ ์ •ํ•ฉ๋‹ˆ๋‹ค.
  2. ๊ธฐ์กด ํŒจํ„ด ์ฐธ๊ณ  โ€” ์ด๋ฏธ ์žˆ๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์™€ ๊ณต์‹ ๋ฌธ์„œ(flutter_test, bloc_test, patrol ๋“ฑ)๋ฅผ ์‚ดํŽด ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ๋งž์ถฅ๋‹ˆ๋‹ค.
  3. ๊ฒ€์‚ฌ ์ฝ”๋“œ ์ž‘์„ฑ โ€” ์ •ํ•ด์ง„ ํด๋” ๊ตฌ์กฐ์— ๋งž์ถฐ, "์ค€๋น„ โ†’ ์‹คํ–‰ โ†’ ํ™•์ธ" ์ˆœ์„œ์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ง„์งœ ์˜์กด์„ฑ ๋Œ€์‹  ๊ฐ€์งœ(Mock)๋ฅผ ๋ผ์›Œ ์•ˆ์ „ํ•˜๊ฒŒ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค.
  4. BDD๋Š” ํ•œ ๋ฒˆ์— ๋‘ ๋ฒŒ โ€” .feature ์‹œ๋‚˜๋ฆฌ์˜ค ํ•˜๋‚˜๋กœ ์œ„์ ฏ์šฉยทPatrol์šฉ ๊ฒ€์‚ฌ ํŒŒ์ผ์ด ํ•จ๊ป˜ ๋งŒ๋“ค์–ด์ง‘๋‹ˆ๋‹ค.
  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#

/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}.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);
    });
  });
}

.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.dart generated 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 displayed

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

# 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-only

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 should use TestDriver (recommended) or WidgetTester
  • K class for centralized widget Key management
  • @widget-only, @patrol-only tags for test target control

Coverage#

  • Target minimum 80% code coverage
  • UseCase 100% testing required
  • BLoC main flow testing required

MCP Integration#

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

Examples#

Generate UseCase Test#

/test unit GetUserUseCase --feature auth

Generate BLoC Test#

/test bloc HomeBloc --feature home

Generate Widget Test#

/test widget LoginPage --feature auth

Generate BDD Scenario (Dual Test)#

/test bdd community

Patrol E2E#

/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