Testing in Flutter

Flutter has excellent built-in testing support at three levels: unit, widget, and integration tests.

Unit Tests

Dart
// test/models/user_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/models/user.dart';

void main() {
  group('User', () {
    test('fromJson creates valid User', () {
      final json = {
        'id': '1',
        'name': 'Alice',
        'email': 'alice@example.com',
        'created_at': '2024-01-01T00:00:00Z',
      };

      final user = User.fromJson(json);

      expect(user.id, '1');
      expect(user.name, 'Alice');
      expect(user.email, 'alice@example.com');
    });

    test('toJson returns correct map', () {
      final user = User(
        id: '1',
        name: 'Alice',
        email: 'alice@example.com',
        createdAt: DateTime(2024),
      );

      final json = user.toJson();

      expect(json['name'], 'Alice');
      expect(json['email'], 'alice@example.com');
      expect(json.containsKey('id'), false); // Not sent in POST
    });

    test('copyWith preserves unchanged fields', () {
      final user = User(
        id: '1',
        name: 'Alice',
        email: 'alice@example.com',
        createdAt: DateTime(2024),
      );

      final updated = user.copyWith(name: 'Bob');

      expect(updated.name, 'Bob');
      expect(updated.email, 'alice@example.com');
      expect(updated.id, '1');
    });
  });
}

Widget Tests

Dart
// test/widgets/counter_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/counter.dart';

void main() {
  group('Counter Widget', () {
    testWidgets('displays initial count of 0', (tester) async {
      await tester.pumpWidget(MaterialApp(home: Counter()));

      expect(find.text('Count: 0'), findsOneWidget);
      expect(find.text('Count: 1'), findsNothing);
    });

    testWidgets('increments count when button is tapped', (tester) async {
      await tester.pumpWidget(MaterialApp(home: Counter()));

      // Find and tap the button
      await tester.tap(find.byIcon(Icons.add));
      await tester.pump();  // Rebuild after setState

      expect(find.text('Count: 1'), findsOneWidget);
    });

    testWidgets('increments multiple times', (tester) async {
      await tester.pumpWidget(MaterialApp(home: Counter()));

      await tester.tap(find.byIcon(Icons.add));
      await tester.pump();
      await tester.tap(find.byIcon(Icons.add));
      await tester.pump();
      await tester.tap(find.byIcon(Icons.add));
      await tester.pump();

      expect(find.text('Count: 3'), findsOneWidget);
    });

    testWidgets('has an add button', (tester) async {
      await tester.pumpWidget(MaterialApp(home: Counter()));

      expect(find.byIcon(Icons.add), findsOneWidget);
    });
  });
}

Testing Forms

Dart
testWidgets('login form validates empty fields', (tester) async {
  await tester.pumpWidget(MaterialApp(home: LoginScreen()));

  // Submit without filling in fields
  await tester.tap(find.byType(ElevatedButton));
  await tester.pump();

  // Should show validation errors
  expect(find.text('Email is required'), findsOneWidget);
  expect(find.text('Password is required'), findsOneWidget);
});

testWidgets('login form submits valid data', (tester) async {
  await tester.pumpWidget(MaterialApp(home: LoginScreen()));

  // Fill in fields
  await tester.enterText(find.byKey(Key('email')), 'test@example.com');
  await tester.enterText(find.byKey(Key('password')), 'password123');
  
  // Submit
  await tester.tap(find.byType(ElevatedButton));
  await tester.pumpAndSettle();  // Wait for async operations

  // Should navigate to home
  expect(find.byType(HomeScreen), findsOneWidget);
});

Mocking

Dart
// test/services/user_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:my_app/services/api_service.dart';

@GenerateMocks([http.Client])
import 'user_service_test.mocks.dart';

void main() {
  group('ApiService', () {
    late MockClient mockClient;
    late ApiService apiService;

    setUp(() {
      mockClient = MockClient();
      apiService = ApiService(client: mockClient);
    });

    test('getUsers returns list of users on 200', () async {
      when(mockClient.get(any, headers: anyNamed('headers'))).thenAnswer(
        (_) async => http.Response(
          '[{"id":"1","name":"Alice","email":"alice@test.com","created_at":"2024-01-01"}]',
          200,
        ),
      );

      final users = await apiService.getUsers();

      expect(users.length, 1);
      expect(users[0].name, 'Alice');
    });

    test('getUsers throws on non-200', () async {
      when(mockClient.get(any, headers: anyNamed('headers'))).thenAnswer(
        (_) async => http.Response('Not Found', 404),
      );

      expect(() => apiService.getUsers(), throwsA(isA<ApiException>()));
    });
  });
}

Integration Tests

Dart
// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('full login flow', (tester) async {
    app.main();
    await tester.pumpAndSettle();

    // Verify we start on login screen
    expect(find.text('Login'), findsOneWidget);

    // Enter credentials
    await tester.enterText(find.byKey(Key('email')), 'test@example.com');
    await tester.enterText(find.byKey(Key('password')), 'password123');
    await tester.tap(find.byType(ElevatedButton));
    await tester.pumpAndSettle();

    // Should be on home screen
    expect(find.text('Welcome'), findsOneWidget);
  });
}

Running Tests

Bash
# Unit and widget tests
flutter test

# With coverage
flutter test --coverage

# Specific file
flutter test test/models/user_test.dart

# Watch mode
flutter test --watch

# Integration tests (needs device/emulator)
flutter test integration_test/

# Generate mock files
dart run build_runner build

Test Directory Structure

Code
test/
├── models/
│   ├── user_test.dart
│   └── post_test.dart
├── services/
│   ├── api_service_test.dart
│   └── auth_service_test.dart
├── widgets/
│   ├── counter_test.dart
│   └── login_form_test.dart
└── helpers/
    └── test_helpers.dart

integration_test/
└── app_test.dart

💡 Tip: Aim for 80%+ unit test coverage. Widget tests should cover all interactive elements. Integration tests are for critical user flows only.