LogoSkills

test

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

ํ•ญ๋ชฉ๋‚ด์šฉ
ToolsRead, Edit, Write, Bash, Glob, Grep
Modelsonnet
Skillstest

Test Agent#

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_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 widget tests (TestDriver + bdd_test_gen)
    • 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_test/bloc_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.state, const UserState.initial());
    });

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

    blocTest<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 BlocBase<S>, S>(
    Widget widget, {
    required B bloc,
  }) async {
    await pumpWidget(
      MaterialApp(
        home: BlocProvider.value(
          value: bloc,
          child: widget,
        ),
      ),
    );
  }
}

Widget Test#

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

class MockUserBloc extends MockBloc<UserEvent, UserState>
    implements UserBloc {}

void main() {
  late MockUserBloc mockBloc;

  setUp(() {
    mockBloc = MockUserBloc();
  });

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

    testWidgets('displays user information', (tester) async {
      // Arrange
      when(() => mockBloc.state).thenReturn(UserState.loaded(user: testUser));

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

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

    testWidgets('shows loading indicator when loading', (tester) async {
      // Arrange
      when(() => mockBloc.state).thenReturn(const UserState.loading());

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

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

    testWidgets('triggers refresh on pull', (tester) async {
      // Arrange
      when(() => mockBloc.state).thenReturn(UserState.loaded(user: testUser));

      // Act
      await tester.pumpAppWithBloc(
        const RefreshIndicator(
          onRefresh: () async {},
          child: UserCard(),
        ),
        bloc: mockBloc,
      );
      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โ€”
WidgetTestDriverWidget test driverWidgetTester
PatrolTestDriver E2E test driver PatrolIntegrationTester
KCentralized widget Keysโ€”

Step functions taking TestDriver as parameter are reusable across both widget and E2E tests.

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

# BDD tests
melos run test:bdd
melos run test:bdd:select

# Code generation (BDD โ†’ _test.dart)
melos run build

# Update golden tests
flutter test --update-goldens

# Patrol E2E
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
  • build.yaml with bdd_test_gen or bdd_widget_test enabled
  • 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