home / skills / kaakati / rails-enterprise-dev / testing-patterns

This skill helps you implement comprehensive testing patterns including unit, widget, integration, and golden tests with mocking strategies.

npx playbooks add skill kaakati/rails-enterprise-dev --skill testing-patterns

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
8.1 KB
---
name: "Testing Patterns"
description: "Unit, widget, integration, and golden test patterns with mocking strategies"
version: "1.0.0"
---

# Testing Patterns

## Unit Tests (Domain & Data Layers)

### Use Case Testing

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:dartz/dartz.dart';

class MockUserRepository extends Mock implements UserRepository {}

void main() {
  late GetUser useCase;
  late MockUserRepository mockRepository;

  setUp(() {
    mockRepository = MockUserRepository();
    useCase = GetUser(mockRepository);
  });

  group('GetUser', () {
    final tUser = User(
      id: '1',
      name: 'Test User',
      email: '[email protected]',
      createdAt: DateTime(2024),
    );

    test('should return user when repository call is successful', () async {
      // Arrange
      when(() => mockRepository.getUser('1'))
          .thenAnswer((_) async => Right(tUser));

      // Act
      final result = await useCase('1');

      // Assert
      expect(result, Right(tUser));
      verify(() => mockRepository.getUser('1')).called(1);
      verifyNoMoreInteractions(mockRepository);
    });

    test('should return ServerFailure when repository fails', () async {
      // Arrange
      when(() => mockRepository.getUser('1'))
          .thenAnswer((_) async => Left(ServerFailure('Error')));

      // Act
      final result = await useCase('1');

      // Assert
      expect(result, isA<Left>());
      verify(() => mockRepository.getUser('1')).called(1);
    });
  });
}
```

### Repository Testing

```dart
class MockUserProvider extends Mock implements UserProvider {}
class MockUserLocalSource extends Mock implements UserLocalSource {}
class MockNetworkInfo extends Mock implements NetworkInfo {}

void main() {
  late UserRepositoryImpl repository;
  late MockUserProvider mockProvider;
  late MockUserLocalSource mockLocalSource;
  late MockNetworkInfo mockNetworkInfo;

  setUp(() {
    mockProvider = MockUserProvider();
    mockLocalSource = MockUserLocalSource();
    mockNetworkInfo = MockNetworkInfo();
    repository = UserRepositoryImpl(
      mockProvider,
      mockLocalSource,
      mockNetworkInfo,
    );
  });

  group('getUser', () {
    final tUserModel = UserModel(
      id: '1',
      name: 'Test',
      email: '[email protected]',
      createdAt: DateTime(2024),
    );

    test('should return remote data when online', () async {
      // Arrange
      when(() => mockNetworkInfo.isConnected).thenAnswer((_) async => true);
      when(() => mockProvider.fetchUser('1'))
          .thenAnswer((_) async => tUserModel);
      when(() => mockLocalSource.cacheUser(tUserModel))
          .thenAnswer((_) async => {});

      // Act
      final result = await repository.getUser('1');

      // Assert
      verify(() => mockProvider.fetchUser('1'));
      verify(() => mockLocalSource.cacheUser(tUserModel));
      expect(result, Right(tUserModel.toEntity()));
    });

    test('should return cached data when offline', () async {
      // Arrange
      when(() => mockNetworkInfo.isConnected).thenAnswer((_) async => false);
      when(() => mockLocalSource.getCachedUser('1'))
          .thenAnswer((_) async => tUserModel);

      // Act
      final result = await repository.getUser('1');

      // Assert
      verify(() => mockLocalSource.getCachedUser('1'));
      verifyNever(() => mockProvider.fetchUser('1'));
      expect(result, Right(tUserModel.toEntity()));
    });
  });
}
```

### Controller Testing

```dart
class MockGetUser extends Mock implements GetUser {}

void main() {
  late UserController controller;
  late MockGetUser mockGetUser;

  setUp(() {
    mockGetUser = MockGetUser();
    controller = UserController(getUserUseCase: mockGetUser);
  });

  tearDown(() {
    controller.dispose();
  });

  group('UserController', () {
    final tUser = User(
      id: '1',
      name: 'Test',
      email: '[email protected]',
      createdAt: DateTime(2024),
    );

    test('initial state should be empty', () {
      expect(controller.user, null);
      expect(controller.isLoading, false);
      expect(controller.error, null);
    });

    test('should load user successfully', () async {
      // Arrange
      when(() => mockGetUser('1')).thenAnswer((_) async => Right(tUser));

      // Act
      await controller.loadUser('1');

      // Assert
      expect(controller.user, tUser);
      expect(controller.isLoading, false);
      expect(controller.error, null);
    });

    test('should handle error', () async {
      // Arrange
      when(() => mockGetUser('1'))
          .thenAnswer((_) async => Left(ServerFailure('Error')));

      // Act
      await controller.loadUser('1');

      // Assert
      expect(controller.user, null);
      expect(controller.isLoading, false);
      expect(controller.error, isNotNull);
    });
  });
}
```

## Widget Tests

```dart
void main() {
  testWidgets('UserPage displays loading indicator', (tester) async {
    // Arrange
    final mockController = MockUserController();
    when(() => mockController.isLoading).thenReturn(true);
    when(() => mockController.user).thenReturn(null);
    when(() => mockController.error).thenReturn(null);
    
    Get.put<UserController>(mockController);

    // Act
    await tester.pumpWidget(
      GetMaterialApp(home: UserPage()),
    );

    // Assert
    expect(find.byType(CircularProgressIndicator), findsOneWidget);
    
    // Cleanup
    Get.delete<UserController>();
  });

  testWidgets('UserPage displays user data', (tester) async {
    // Arrange
    final tUser = User(
      id: '1',
      name: 'John Doe',
      email: '[email protected]',
      createdAt: DateTime(2024),
    );
    
    final mockController = MockUserController();
    when(() => mockController.isLoading).thenReturn(false);
    when(() => mockController.user).thenReturn(tUser);
    when(() => mockController.error).thenReturn(null);
    
    Get.put<UserController>(mockController);

    // Act
    await tester.pumpWidget(
      GetMaterialApp(home: UserPage()),
    );

    // Assert
    expect(find.text('John Doe'), findsOneWidget);
    expect(find.text('[email protected]'), findsOneWidget);
    
    // Cleanup
    Get.delete<UserController>();
  });

  testWidgets('UserPage displays error', (tester) async {
    // Arrange
    final mockController = MockUserController();
    when(() => mockController.isLoading).thenReturn(false);
    when(() => mockController.user).thenReturn(null);
    when(() => mockController.error).thenReturn('Server error');
    
    Get.put<UserController>(mockController);

    // Act
    await tester.pumpWidget(
      GetMaterialApp(home: UserPage()),
    );

    // Assert
    expect(find.text('Server error'), findsOneWidget);
    expect(find.byIcon(Icons.error_outline), findsOneWidget);
    
    // Cleanup
    Get.delete<UserController>();
  });
}
```

## Integration Tests

```dart
void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('User Flow Integration Tests', () {
    testWidgets('complete user login flow', (tester) async {
      // Start app
      await tester.pumpWidget(MyApp());
      await tester.pumpAndSettle();

      // Navigate to login
      await tester.tap(find.text('Login'));
      await tester.pumpAndSettle();

      // Enter credentials
      await tester.enterText(find.byType(TextField).at(0), '[email protected]');
      await tester.enterText(find.byType(TextField).at(1), 'password');
      await tester.pumpAndSettle();

      // Submit
      await tester.tap(find.text('Submit'));
      await tester.pumpAndSettle();

      // Verify navigation to home
      expect(find.text('Home'), findsOneWidget);
    });
  });
}
```

## Golden Tests

```dart
void main() {
  testWidgets('UserCard golden test', (tester) async {
    final tUser = User(
      id: '1',
      name: 'John Doe',
      email: '[email protected]',
      createdAt: DateTime(2024),
    );

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: UserCard(user: tUser),
        ),
      ),
    );

    await expectLater(
      find.byType(UserCard),
      matchesGoldenFile('goldens/user_card.png'),
    );
  });
}
```

Overview

This skill documents pragmatic testing patterns for Flutter/Dart projects: unit, repository, controller, widget, integration, and golden tests with clear mocking strategies. It presents repeatable examples that fit enterprise Rails-aligned workflows but remain portable across any codebase. The focus is on reliable, maintainable tests that support CI quality gates and multi-agent orchestration.

How this skill works

The skill outlines what to inspect and how to mock at each layer: use-case and controller units with mock use cases/repositories, repository tests that toggle network/local paths, widget tests that inject mocked controllers, integration tests driving full flows, and golden tests validating visual output. Each pattern shows arrange-act-assert structure, verification of interactions, and cleanup steps to avoid test pollution.

When to use it

  • Unit tests for domain logic and small data transformations
  • Repository tests when behavior depends on network/local sources and caching
  • Controller tests to validate state management and error handling
  • Widget tests for UI logic with injected mock controllers
  • Integration tests for critical user flows across screens and services
  • Golden tests to lock visual regressions for key components

Best practices

  • Keep unit tests focused: one behavior per test and explicit arrange-act-assert
  • Prefer mocks for external dependencies and verify interactions to catch regressions
  • Test both online and offline repository branches and cache behavior
  • Use dependency injection (or service locators) to inject mocks in widget tests and clean up after each test
  • Limit golden tests to stable components and run them in consistent environments
  • Run integration tests selectively in CI and keep them deterministic by avoiding flaky waits

Example use cases

  • Validate a GetUser use-case returns domain entities and handles failures
  • Ensure repository fetches remote data when online and returns cached data when offline
  • Confirm controller toggles loading and error states correctly for UI consumers
  • Assert UserPage shows loader, data, or error by injecting a mocked controller
  • Run a full login flow in an integration test to verify end-to-end navigation
  • Capture a UserCard golden to prevent visual regressions in the app shell

FAQ

How do I choose between widget and integration tests?

Use widget tests for isolated UI behavior with mocked dependencies; use integration tests for end-to-end flows involving navigation, platform plugins, or real data paths.

When should I use golden tests?

Use golden tests for stable, visually critical components where pixel-perfect output matters, and run them in a consistent environment to avoid false positives.

How to avoid flaky integration tests?

Make tests deterministic: prefer pumpAndSettle over arbitrary delays, stub network calls where feasible, and keep environment setup consistent in CI.