Back to Posts

Testing Flutter Apps with Mock Data

8 min read

Testing is a crucial aspect of Flutter app development that ensures reliability and maintainability. This comprehensive guide will walk you through various testing strategies using mock data, from basic unit tests to complex integration tests.

Understanding Mock Data in Testing

1. Why Use Mock Data?

  • Isolation: Test components independently
  • Predictability: Control test scenarios
  • Speed: Faster test execution
  • Reliability: Consistent test results
  • Edge Cases: Test error conditions

2. Types of Tests

  • Unit Tests: Test individual functions/classes
  • Widget Tests: Test UI components
  • Integration Tests: Test app flows
  • Golden Tests: Test visual appearance

Setting Up Testing Environment

1. Required Packages

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^5.0.0
  build_runner: ^2.0.0
  integration_test:
    sdk: flutter
  flutter_driver:
    sdk: flutter
  golden_toolkit: ^0.12.0

2. Mock Data Generation

// test/mocks/user_mock.dart
import 'package:mockito/annotations.dart';
import 'package:your_app/services/api_service.dart';
import 'package:your_app/services/database_service.dart';
import 'package:your_app/services/auth_service.dart';

@GenerateMocks([
  ApiService,
  DatabaseService,
  AuthService,
])
void main() {}

Run the generator:

flutter pub run build_runner build

Unit Testing with Mock Data

1. Basic Mock Setup

import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:your_app/services/api_service.dart';
import 'package:your_app/models/user.dart';
import 'mocks/api_service_test.mocks.dart';

void main() {
  group('ApiService Tests', () {
    late MockApiService mockApiService;
    late UserRepository userRepository;

    setUp(() {
      mockApiService = MockApiService();
      userRepository = UserRepository(apiService: mockApiService);
    });

    test('Fetch users returns a list of users', () async {
      // Arrange
      final mockUsers = [
        User(id: 1, name: 'John Doe', email: 'john.doe@example.com'),
        User(id: 2, name: 'Jane Smith', email: 'jane.smith@example.com'),
      ];
      
      when(mockApiService.fetchUsers()).thenAnswer((_) async => mockUsers);

      // Act
      final users = await userRepository.getUsers();

      // Assert
      expect(users, isNotEmpty);
      expect(users.length, 2);
      expect(users.first.name, 'John Doe');
      verify(mockApiService.fetchUsers()).called(1);
    });

    test('Handles API errors gracefully', () async {
      // Arrange
      when(mockApiService.fetchUsers())
          .thenThrow(ApiException('Network error'));

      // Act & Assert
      expect(
        () => userRepository.getUsers(),
        throwsA(isA<ApiException>()),
      );
    });
  });
}

2. Advanced Mocking Techniques

// Mocking streams
test('Stream subscription test', () {
  final mockStream = StreamController<User>();
  when(mockApiService.userStream()).thenAnswer((_) => mockStream.stream);

  // Test stream behavior
  mockStream.add(User(id: 1, name: 'Test User'));
  // Verify stream handling
});

// Mocking complex objects
test('Complex object mocking', () {
  final mockUser = MockUser();
  when(mockUser.name).thenReturn('Mock User');
  when(mockUser.isActive).thenReturn(true);
  
  // Test with mocked object
});

Widget Testing with Mock Data

1. Basic Widget Test

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:your_app/widgets/user_list.dart';
import 'package:your_app/services/user_service.dart';
import 'mocks/user_service_test.mocks.dart';

void main() {
  group('UserList Widget Tests', () {
    late MockUserService mockUserService;

    setUp(() {
      mockUserService = MockUserService();
    });

    testWidgets('Displays list of users', (WidgetTester tester) async {
      // Arrange
      final mockUsers = [
        User(id: 1, name: 'John Doe'),
        User(id: 2, name: 'Jane Smith'),
      ];
      
      when(mockUserService.getUsers()).thenAnswer((_) async => mockUsers);

      // Act
      await tester.pumpWidget(
        MaterialApp(
          home: UserList(userService: mockUserService),
        ),
      );
      await tester.pumpAndSettle();

      // Assert
      expect(find.text('John Doe'), findsOneWidget);
      expect(find.text('Jane Smith'), findsOneWidget);
    });

    testWidgets('Shows loading indicator', (WidgetTester tester) async {
      // Arrange
      when(mockUserService.getUsers())
          .thenAnswer((_) => Future.delayed(Duration(seconds: 1)));

      // Act
      await tester.pumpWidget(
        MaterialApp(
          home: UserList(userService: mockUserService),
        ),
      );
      await tester.pump();

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

2. Testing User Interactions

testWidgets('Handles user tap', (WidgetTester tester) async {
  // Arrange
  final mockUsers = [User(id: 1, name: 'Test User')];
  when(mockUserService.getUsers()).thenAnswer((_) async => mockUsers);

  // Act
  await tester.pumpWidget(
    MaterialApp(
      home: UserList(userService: mockUserService),
    ),
  );
  await tester.pumpAndSettle();

  // Tap on user
  await tester.tap(find.text('Test User'));
  await tester.pumpAndSettle();

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

Integration Testing with Mock Data

1. Basic Integration Test

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:your_app/main.dart' as app;
import 'package:your_app/services/api_service.dart';
import 'mocks/api_service_test.mocks.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('App Integration Tests', () {
    late MockApiService mockApiService;

    setUp(() {
      mockApiService = MockApiService();
      // Inject mock service
      app.apiService = mockApiService;
    });

    testWidgets('Complete user flow', (WidgetTester tester) async {
      // Arrange
      final mockUsers = [
        User(id: 1, name: 'John Doe'),
        User(id: 2, name: 'Jane Smith'),
      ];
      
      when(mockApiService.fetchUsers()).thenAnswer((_) async => mockUsers);

      // Act
      app.main();
      await tester.pumpAndSettle();

      // Assert
      expect(find.text('John Doe'), findsOneWidget);
      expect(find.text('Jane Smith'), findsOneWidget);

      // Test navigation
      await tester.tap(find.text('John Doe'));
      await tester.pumpAndSettle();
      expect(find.byType(UserDetails), findsOneWidget);
    });
  });
}

2. Testing Network Calls

testWidgets('Handles network errors', (WidgetTester tester) async {
  // Arrange
  when(mockApiService.fetchUsers())
      .thenThrow(ApiException('Network error'));

  // Act
  app.main();
  await tester.pumpAndSettle();

  // Assert
  expect(find.text('Error loading users'), findsOneWidget);
  expect(find.byType(RetryButton), findsOneWidget);
});

Best Practices

1. Mock Data Organization

// test/mock_data/user_mock_data.dart
class UserMockData {
  static List<User> get mockUsers => [
    User(id: 1, name: 'John Doe'),
    User(id: 2, name: 'Jane Smith'),
  ];

  static User get mockUser => User(
    id: 1,
    name: 'Test User',
    email: 'test@example.com',
  );
}

2. Test Data Factories

// test/factories/user_factory.dart
class UserFactory {
  static User createUser({
    int? id,
    String? name,
    String? email,
  }) {
    return User(
      id: id ?? 1,
      name: name ?? 'Test User',
      email: email ?? 'test@example.com',
    );
  }
}

3. Test Helpers

// test/helpers/test_helpers.dart
extension WidgetTesterExtension on WidgetTester {
  Future<void> pumpUntilFound(
    Finder finder, {
    Duration timeout = const Duration(seconds: 30),
  }) async {
    bool timerDone = false;
    final timer = Timer(timeout, () => timerDone = true);
    
    while (!timerDone) {
      await pump();
      if (finder.evaluate().isNotEmpty) {
        timer.cancel();
        return;
      }
    }
    
    throw Exception('Widget not found after timeout');
  }
}

Common Testing Patterns

1. Testing State Changes

test('State changes correctly', () {
  final bloc = UserBloc();
  
  expect(
    bloc.stream,
    emitsInOrder([
      UserInitial(),
      UserLoading(),
      UserLoaded(mockUsers),
    ]),
  );
  
  bloc.add(FetchUsers());
});

2. Testing Error Handling

test('Handles errors correctly', () {
  when(mockApiService.fetchUsers())
      .thenThrow(ApiException('Test error'));
  
  expect(
    bloc.stream,
    emitsInOrder([
      UserInitial(),
      UserLoading(),
      UserError('Test error'),
    ]),
  );
  
  bloc.add(FetchUsers());
});

Conclusion

Effective testing with mock data requires:

  • Proper setup of testing environment
  • Understanding different types of tests
  • Using appropriate mocking techniques
  • Following best practices
  • Maintaining test data organization

Remember to:

  • Keep tests isolated and independent
  • Use meaningful mock data
  • Test edge cases and error conditions
  • Maintain test readability
  • Update tests with code changes

By following these guidelines and implementing the provided examples, you can build a robust testing strategy for your Flutter applications.