LogoSkills

test

Flutter test specialist. Used for unit/BLoC/widget/golden/BDD/Patrol test writing

Test Agent#

ํ•ญ๋ชฉ๋‚ด์šฉ
๋ชจ๋ธsonnet
์‚ฌ์šฉ ๋„๊ตฌ Read , Edit , Write , Bash , Glob , Grep
์—ฐ๊ณ„ ์Šคํ‚ฌtest

An agent specializing in unit tests, widget tests, golden tests, BDD tests, and Patrol E2E tests.

Triggers#

@test or auto-activated on detecting the following keywords:

  • Test, test, verification
  • Unit tests, widget tests, golden tests
  • BDD, feature file, Gherkin, Step
  • Patrol, E2E, integration test
  • mocktail, bloc_signals_test, TestDriver

Role#

  1. Test Strategy

    • Test pyramid application
    • Coverage goal setting
    • Test priority determination
  2. Test Implementation

    • Unit tests (UseCase, Repository)
    • BLoC tests
    • Widget tests
    • Golden tests
    • BDD (Gherkin) scenarios โ†’ hand-written Patrol E2E tests only (TestDriver, no code generator) under app/{app}/integration_test/ โ€” BDD never generates widget tests, and no BDD scaffold exists inside the feature package
    • Patrol E2E tests
  3. Mock/Stub

    • Mocktail utilization
    • Test data management
    • Dependency injection

Test Structure#

test/
โ”œโ”€โ”€ domain/
โ”‚   โ””โ”€โ”€ usecase/
โ”‚       โ””โ”€โ”€ get_user_usecase_test.dart
โ”œโ”€โ”€ data/
โ”‚   โ””โ”€โ”€ repository/
โ”‚       โ””โ”€โ”€ user_repository_test.dart
โ”œโ”€โ”€ presentation/
โ”‚   โ”œโ”€โ”€ bloc/
โ”‚   โ”‚   โ””โ”€โ”€ user_bloc_test.dart
โ”‚   โ””โ”€โ”€ widget/
โ”‚       โ””โ”€โ”€ user_card_test.dart
โ”œโ”€โ”€ fixtures/
โ”‚   โ””โ”€โ”€ test_data.dart
โ”œโ”€โ”€ helpers/
โ”‚   โ””โ”€โ”€ pump_app.dart
โ””โ”€โ”€ goldens/
    โ””โ”€โ”€ user_card_golden_test.dart

UseCase Tests#

import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:dependencies/dependencies.dart';

class MockIUserRepository extends Mock implements IUserRepository {}

void main() {
  late GetUserUseCase useCase;
  late MockIUserRepository mockRepository;

  setUp(() {
    mockRepository = MockIUserRepository();
    useCase = GetUserUseCase(mockRepository);
  });

  setUpAll(() {
    registerFallbackValue(const GetUserParams(id: 0));
  });

  group('GetUserUseCase', () {
    final testUser = User(id: 1, name: 'Test', email: 'test@test.com');

    test('should return User when repository succeeds', () async {
      // Arrange
      when(() => mockRepository.getUser(any()))
          .thenAnswer((_) async => right(testUser));

      // Act
      final result = await useCase(const GetUserParams(id: 1));

      // Assert
      expect(result, right(testUser));
      verify(() => mockRepository.getUser(1)).called(1);
    });

    test('should return Failure when repository fails', () async {
      // Arrange
      final failure = ServerFailure('Server error');
      when(() => mockRepository.getUser(any()))
          .thenAnswer((_) async => left(failure));

      // Act
      final result = await useCase(const GetUserParams(id: 1));

      // Assert
      expect(result, left(failure));
    });
  });
}

BLoC Tests#

import 'package:bloc_signals_test/bloc_signals_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockGetUserUseCase extends Mock implements GetUserUseCase {}

void main() {
  late UserBloc bloc;
  late MockGetUserUseCase mockGetUserUseCase;

  final testUser = User(id: 1, name: 'Test', email: 'test@test.com');

  setUp(() {
    mockGetUserUseCase = MockGetUserUseCase();
    bloc = UserBloc(mockGetUserUseCase);
  });

  setUpAll(() {
    registerFallbackValue(const GetUserParams(id: 0));
  });

  tearDown(() {
    bloc.close();
  });

  group('UserBloc', () {
    test('initial state is UserState.initial()', () {
      expect(bloc.stateValue, const UserState.initial());
    });

    blocSignalTest<UserBloc, UserState>(
      'emits [loading, loaded] when load succeeds',
      setUp: () {
        when(() => mockGetUserUseCase(any()))
            .thenAnswer((_) async => right(testUser));
      },
      build: () => bloc,
      act: (bloc) => bloc.add(const UserEvent.load(userId: 1)),
      expect: () => [
        const UserState.loading(),
        UserState.loaded(user: testUser),
      ],
      verify: (_) {
        verify(() => mockGetUserUseCase(const GetUserParams(id: 1))).called(1);
      },
    );

    blocSignalTest<UserBloc, UserState>(
      'emits [loading, error] when load fails',
      setUp: () {
        when(() => mockGetUserUseCase(any()))
            .thenAnswer((_) async => left(ServerFailure('Error')));
      },
      build: () => bloc,
      act: (bloc) => bloc.add(const UserEvent.load(userId: 1)),
      expect: () => [
        const UserState.loading(),
        isA<UserState>(),
      ],
    );
  });
}

Widget Tests#

Test Helper#

// test/helpers/pump_app.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';

extension PumpApp on WidgetTester {
  Future<void> pumpApp(
    Widget widget, {
    List<Override> overrides = const [],
    NavigatorObserver? navigatorObserver,
  }) async {
    await pumpWidget(
      MaterialApp(
        home: widget,
        navigatorObservers: [
          if (navigatorObserver != null) navigatorObserver,
        ],
      ),
    );
  }

  Future<void> pumpAppWithBloc<B extends BlocSignalBase<S>, S>(
    Widget widget, {
    required B bloc,
  }) async {
    await pumpWidget(
      MaterialApp(
        home: BlocSignalProvider.value(
          value: bloc,
          child: widget,
        ),
      ),
    );
  }
}

Widget Test#

import 'package:flutter_test/flutter_test.dart';
import 'package:bloc_signals_test/bloc_signals_test.dart';
import 'package:mocktail/mocktail.dart';

// bloc_signals ์—๋Š” MockBloc/whenListen ์ด ์—†๊ณ , ์ƒํƒœ๋Š” signal ์ด๋ฏ€๋กœ
// ๋ชฉ์œผ๋กœ ๋Œ€์ฒดํ•  ์ˆ˜ ์—†๋‹ค. ์‹ค์ œ ์ปจํ…Œ์ด๋„ˆ๋ฅผ initialState ๋กœ ์‹œ๋“œํ•ด์„œ ์“ด๋‹ค.
void main() {
  late MockGetUserUseCase mockGetUser;

  setUp(() {
    mockGetUser = MockGetUserUseCase();
  });

  UserBloc seeded(UserState state) {
    final bloc = UserBloc(getUserUseCase: mockGetUser, initialState: state);
    addTearDown(bloc.close);
    return bloc;
  }

  group('UserCard', () {
    final testUser = User(id: 1, name: 'Test User', email: 'test@test.com');

    testWidgets('displays user information', (tester) async {
      // Arrange
      final bloc = seeded(UserState.loaded(user: testUser));

      // Act
      await tester.pumpAppWithBloc(
        const UserCard(),
        bloc: bloc,
      );

      // Assert
      expect(find.text('Test User'), findsOneWidget);
      expect(find.text('test@test.com'), findsOneWidget);
    });

    testWidgets('shows loading indicator when loading', (tester) async {
      // Arrange
      final bloc = seeded(const UserState.loading());

      // Act
      await tester.pumpAppWithBloc(
        const UserCard(),
        bloc: bloc,
      );

      // Assert
      expect(find.byType(CircularProgressIndicator), findsOneWidget);
    });

    testWidgets('triggers refresh on pull', (tester) async {
      // Arrange
      final bloc = seeded(UserState.loaded(user: testUser));

      // Act
      await tester.pumpAppWithBloc(
        const RefreshIndicator(
          onRefresh: () async {},
          child: UserCard(),
        ),
        bloc: bloc,
      );
      await tester.fling(find.byType(UserCard), const Offset(0, 300), 500);
      await tester.pumpAndSettle();

      // Assert
      verify(() => mockBloc.add(const UserEvent.refresh())).called(1);
    });
  });
}

Golden Tests#

import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';

void main() {
  group('UserCard Golden Tests', () {
    testGoldens('UserCard renders correctly', (tester) async {
      // Arrange
      final widget = GoldenTestScenario(
        name: 'default',
        child: UserCard(
          user: User(id: 1, name: 'Test User', email: 'test@test.com'),
        ),
      );

      // Act & Assert
      await tester.pumpWidgetBuilder(
        widget,
        surfaceSize: const Size(400, 200),
      );
      await screenMatchesGolden(tester, 'user_card_default');
    });

    testGoldens('UserCard states', (tester) async {
      final builder = DeviceBuilder()
        ..overrideDevicesForAllScenarios(devices: [Device.phone])
        ..addScenario(
          name: 'loading',
          widget: const UserCard.loading(),
        )
        ..addScenario(
          name: 'loaded',
          widget: UserCard(user: testUser),
        )
        ..addScenario(
          name: 'error',
          widget: const UserCard.error(message: 'Failed to load'),
        );

      await tester.pumpDeviceBuilder(builder);
      await screenMatchesGolden(tester, 'user_card_states');
    });
  });
}

Test Data#

// test/fixtures/test_data.dart
class TestData {
  static final user = User(
    id: 1,
    name: 'Test User',
    email: 'test@test.com',
    avatarUrl: 'https://example.com/avatar.png',
    createdAt: DateTime(2024, 1, 1),
  );

  static final users = [
    user,
    User(id: 2, name: 'User 2', email: 'user2@test.com'),
    User(id: 3, name: 'User 3', email: 'user3@test.com'),
  ];

  static final post = Post(
    id: 1,
    title: 'Test Post',
    content: 'Test content',
    authorId: 1,
  );
}

Mock Registration#

// test/helpers/register_fallback_values.dart
void registerAllFallbackValues() {
  registerFallbackValue(const GetUserParams(id: 0));
  registerFallbackValue(const CreateUserParams(name: '', email: ''));
  registerFallbackValue(const UserEvent.load(userId: 0));
  registerFallbackValue(const UserState.initial());
}

BDD Test (TestDriver Pattern)#

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 # ์—๋Ÿฌ ํ‘œ์‹œ

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

TestDriver Key Classes#

ClassPurposeWraps
TestDriverAbstract interfaceโ€”
PatrolTestDriver E2E test driver (only implementation) PatrolIntegrationTester
KCentralized widget Keysโ€”

WidgetTestDriver (used for dual widget+Patrol generation) is retired โ€” BDD scenarios generate Patrol E2E tests only, so PatrolTestDriver is the sole TestDriver implementation. The abstract TestDriver interface itself is kept so step function signatures stay stable if a future execution target is added.

Patrol E2E Test#

import 'package:patrol/patrol.dart';
import 'package:test_driver/test_driver.dart';

void main() {
  patrolTest(
    'Given logged in, When store screen, Then book list displayed',
    config: const PatrolTesterConfig(settleTimeout: Duration(seconds: 15)),
    ($) async {
      final driver = PatrolTestDriver($);
      await driver.tap(K.navStore);
      await driver.settle();
      await driver.expectVisible(K.bookList);
    },
  );
}

Commands#

# Run all tests
melos run test

# Test specific package
melos exec --scope=feature_user --  " flutter test " 

 # Generate coverage
melos run test:with-html-coverage

# Update golden tests
flutter test --update-goldens

# โ›” melos run test:bdd / test:bdd:select / melos run build (BDD โ†’ _test.dart) are retired โ€”
# there is no BDD codegen pipeline anymore. BDD (Gherkin) scenarios run only via hand-written
# Patrol E2E tests:
patrol test --target integration_test/scenarios/smoke_test.dart

# Single file test
flutter test test/domain/usecase/get_user_usecase_test.dart

Checklist#

  • UseCase tests (success/failure cases)
  • BLoC tests (all events)
  • Widget tests (per state)
  • BDD .feature files + Step functions (hand-written, under app/{app}/integration_test/ โ€” none of it lives in the feature package)
  • No build.yaml / codegen step for BDD (Patrol E2E only, hand-written โ€” โ›” bdd_widget_test/bdd_test_gen/co_test_gen are retired; nothing generates .feature โ†’ test files)
  • K class Keys assigned to actual widgets (TestDriver pattern)
  • Generate mock classes
  • Define test data
  • Golden tests (UI components)
  • Coverage 80% or above
  • @bloc: BLoC implementation
  • @feature: Feature structure
  • @flutter-ui: UI components
  • BDD generation: commands/bdd/generate.md
  • BDD patterns: rules/bdd-test-patterns.md