Flutter Testing Guide: Unit, Widget, and Integration Tests

Flutter Testing Guide: Unit, Widget, and Integration Tests

Flutter ships with three testing layers built into the SDK: unit tests (dart:test) for pure Dart logic, widget tests (flutter_test) for individual Flutter widgets in isolation, and integration tests (integration_test) for full app flows on a real device or emulator. This guide covers all three — setup, writing tests, and running them in CI.

Key Takeaways

Flutter has first-class testing support. No third-party test runner needed. flutter test runs unit and widget tests; flutter test integration_test/ runs integration tests.

Widget tests run in a test environment, not a real device. They're fast (seconds) and can test widget behavior, rendering, and interactions. They don't test platform channels or device-specific behavior.

Integration tests run on a real device or emulator. They're slow (minutes) but test real behavior including animations, platform channels, and system dialogs. Use them for critical flows only.

expect() in Flutter tests works like in Dart:test. For widget interactions, await tester.tap(), await tester.pump(), and expect(find.text('Hello'), findsOneWidget) are the core patterns.

Group tests with group(). This mirrors Jasmine/Jest describe. Use it to organize tests logically and get clear, hierarchical output.

Flutter Testing Stack

Type Package Runs on Speed Use for
Unit tests dart:test Dart VM Fast (ms) Business logic, utilities, models
Widget tests flutter_test Flutter test env Medium (seconds) Widgets, state, UI interactions
Integration tests integration_test Real device/emulator Slow (minutes) Full user flows, platform features

Project Structure

lib/
  models/
    user.dart
  services/
    user_service.dart
  widgets/
    user_card.dart
test/
  models/
    user_test.dart       # unit tests
  services/
    user_service_test.dart
  widgets/
    user_card_test.dart  # widget tests
integration_test/
  app_test.dart          # integration tests
pubspec.yaml

Add test dependencies to pubspec.yaml:

dev_dependencies:
  flutter_test:
    sdk: flutter
  integration_test:
    sdk: flutter
  mocktail: ^1.0.0
  bloc_test: ^9.1.0

Unit Tests

Unit tests test pure Dart code without Flutter widgets:

// lib/models/user.dart
class User {
  final String id;
  final String name;
  final String email;
  final UserRole role;

  const User({
    required this.id,
    required this.name,
    required this.email,
    required this.role,
  });

  bool get isAdmin => role == UserRole.admin;

  User copyWith({ String? name, String? email, UserRole? role }) {
    return User(
      id: id,
      name: name ?? this.name,
      email: email ?? this.email,
      role: role ?? this.role,
    );
  }

  @override
  bool operator ==(Object other) =>
      other is User && other.id == id;
  
  @override
  int get hashCode => id.hashCode;
}

enum UserRole { admin, editor, viewer }
// test/models/user_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/models/user.dart';

void main() {
  group('User', () {
    final alice = User(
      id: '1',
      name: 'Alice',
      email: 'alice@example.com',
      role: UserRole.admin,
    );

    group('isAdmin', () {
      test('returns true for admin role', () {
        expect(alice.isAdmin, isTrue);
      });

      test('returns false for non-admin roles', () {
        final viewer = alice.copyWith(role: UserRole.viewer);
        expect(viewer.isAdmin, isFalse);
      });
    });

    group('copyWith', () {
      test('updates name while preserving other fields', () {
        final updated = alice.copyWith(name: 'Alice Smith');
        expect(updated.name, equals('Alice Smith'));
        expect(updated.email, equals(alice.email));
        expect(updated.id, equals(alice.id));
      });

      test('original is unchanged after copyWith', () {
        alice.copyWith(name: 'Different Name');
        expect(alice.name, equals('Alice'));
      });
    });

    group('equality', () {
      test('users with the same id are equal', () {
        final alice2 = alice.copyWith(name: 'Alice Changed');
        expect(alice, equals(alice2));
      });

      test('users with different ids are not equal', () {
        final bob = User(id: '2', name: 'Bob', email: 'bob@example.com', role: UserRole.viewer);
        expect(alice, isNot(equals(bob)));
      });
    });
  });
}

Widget Tests

Widget tests mount individual widgets in a test harness:

// lib/widgets/counter_widget.dart
import 'package:flutter/material.dart';

class CounterWidget extends StatefulWidget {
  const CounterWidget({super.key});

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $_count', key: const Key('count-text')),
        ElevatedButton(
          key: const Key('increment-btn'),
          onPressed: () => setState(() => _count++),
          child: const Text('+'),
        ),
        ElevatedButton(
          key: const Key('decrement-btn'),
          onPressed: () => setState(() { if (_count > 0) _count--; }),
          child: const Text('-'),
        ),
      ],
    );
  }
}
// test/widgets/counter_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/counter_widget.dart';

void main() {
  group('CounterWidget', () {
    testWidgets('renders with initial count of 0', (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(home: CounterWidget()),
      );

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

    testWidgets('increments count on + button tap', (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(home: CounterWidget()),
      );

      await tester.tap(find.byKey(const Key('increment-btn')));
      await tester.pump();  // rebuild the widget

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

    testWidgets('does not decrement below 0', (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(home: CounterWidget()),
      );

      await tester.tap(find.byKey(const Key('decrement-btn')));
      await tester.pump();

      expect(find.text('Count: 0'), findsOneWidget);  // stays at 0
    });
  });
}

Integration Tests

Integration tests run on a real device/emulator and test the full app:

// 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();

  group('App', () {
    testWidgets('user can login and see the dashboard', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      // Find login form
      await tester.enterText(find.byKey(const Key('email-field')), 'alice@example.com');
      await tester.enterText(find.byKey(const Key('password-field')), 'password123');
      await tester.tap(find.byKey(const Key('login-btn')));
      await tester.pumpAndSettle();

      // Should be on dashboard
      expect(find.text('Dashboard'), findsOneWidget);
      expect(find.text('Welcome, Alice'), findsOneWidget);
    });
  });
}

Running Tests

# Unit and widget tests
flutter <span class="hljs-built_in">test

<span class="hljs-comment"># Specific test file
flutter <span class="hljs-built_in">test <span class="hljs-built_in">test/models/user_test.dart

<span class="hljs-comment"># With coverage
flutter <span class="hljs-built_in">test --coverage
genhtml coverage/lcov.info -o coverage/html

<span class="hljs-comment"># Integration tests (device must be connected)
flutter <span class="hljs-built_in">test integration_test/

<span class="hljs-comment"># Integration tests with specific driver
flutter drive --target=test_driver/app.dart

What Each Level Tests

Unit tests are best for: business logic, model transformations, utility functions, pure Dart calculations.

Widget tests are best for: widget rendering, user interactions (taps, swipes), state changes, navigation between routes.

Integration tests are best for: critical user flows end-to-end, platform channel interactions, camera/location/notifications.

The rest of this cluster covers each layer in depth:

  • Widget testing — WidgetTester, finders, pumping, async widgets
  • Integration testing — integration_test package, Patrol for advanced automation
  • Mocking with mocktail — dependency injection in Flutter tests
  • BLoC testing — bloc_test package patterns
  • Golden tests — screenshot comparison testing

HelpMeTest complements your Flutter test suite with 24/7 monitoring of your backend API and web services. Start free with 10 tests.

Read more