Home
Dart
OOP
Flutter
Contact

Flutter Testing — Unit, Widget & Integration Tests

Write reliable, bug-free Flutter apps by mastering all three levels of testing.

Why Testing Matters

Testing is the backbone of maintainable Flutter applications. Google recommends a testing pyramid: many fast unit tests at the base, fewer widget tests in the middle, and a small number of integration tests at the top.

📊 Testing Pyramid
  • Unit Tests — Fast, test individual functions/methods (bottom, most numerous)
  • Widget Tests — Test UI components in isolation (middle)
  • Integration Tests — Full app flows on real devices (top, fewest)

Each type serves a different purpose:

  • Unit tests verify logic (business rules, data transformations, API parsing)
  • Widget tests verify that widgets render and respond to user interactions correctly
  • Integration tests verify that multiple components work together on a real device/emulator

Unit Tests

Unit tests validate individual functions, methods, or classes. They're the fastest and cheapest to write.

Setup

pubspec.yaml
dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^5.4.0
  build_runner: ^2.4.0

Writing Your First Unit Test

test/counter_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/models/counter.dart';

void main() {
  group('Counter', () {
    late Counter counter;

    setUp(() {
      counter = Counter();
    });

    test('starts at zero', () {
      expect(counter.value, 0);
    });

    test('increments by one', () {
      counter.increment();
      expect(counter.value, 1);
    });

    test('decrements by one', () {
      counter.increment();
      counter.increment();
      counter.decrement();
      expect(counter.value, 1);
    });

    test('does not go below zero', () {
      counter.decrement();
      expect(counter.value, 0);
    });
  });
}

Testing Async Code

test/weather_service_test.dart
test('fetchWeather returns Weather on success', () async {
  final service = WeatherService(client: MockHttpClient());

  final weather = await service.fetchWeather('London');

  expect(weather.city, equals('London'));
  expect(weather.temperature, isA<double>());
});

test('fetchWeather throws on network error', () async {
  final service = WeatherService(client: MockFailingClient());

  expect(
    () => service.fetchWeather('London'),
    throwsA(isA<NetworkException>()),
  );
});
💡 Common Matchers
  • expect(value, equals(expected)) — exact equality
  • expect(value, isNull) / isNotNull
  • expect(value, isA<Type>()) — type checking
  • expect(list, contains(item)) — collection matchers
  • expect(fn, throwsA(matcher)) — exception matching

Widget Tests

Widget tests run in a simulated Flutter environment and verify that widgets render and interact correctly.

test/widget/counter_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/counter_display.dart';

void main() {
  testWidgets('displays the current count', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: CounterDisplay(count: 42),
      ),
    );

    expect(find.text('42'), findsOneWidget);
    expect(find.text('0'), findsNothing);
  });

  testWidgets('tapping + button increments the count', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(home: CounterPage()),
    );

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

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

  testWidgets('shows error snackbar when count is at max', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(home: CounterPage(maxCount: 5)),
    );

    for (var i = 0; i < 6; i++) {
      await tester.tap(find.byIcon(Icons.add));
      await tester.pumpAndSettle();
    }

    expect(find.text('Maximum count reached'), findsOneWidget);
  });
}
⚠️ pump vs pumpAndSettle Use pump() for state changes without animations. Use pumpAndSettle() when animations, timers, or async operations are involved — it waits until all frames are rendered.

Integration Tests

Integration tests run on a real device or emulator and test complete user flows.

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

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

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

    await tester.enterText(
      find.byKey(const Key('email_field')),
      '[email protected]',
    );

    await tester.enterText(
      find.byKey(const Key('password_field')),
      'securePassword123',
    );

    await tester.tap(find.byKey(const Key('login_button')));
    await tester.pumpAndSettle();

    expect(find.text('Welcome!'), findsOneWidget);
  });
}

Running Integration Tests

# Run on a connected device
flutter test integration_test/app_test.dart

# Run with a specific device
flutter test -d <device_id> integration_test/app_test.dart

Mocking with Mockito

Mocks let you isolate the code under test by replacing dependencies with fake implementations.

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

@GenerateMocks([ApiService, http.Client])
import 'mocks.mocks.dart';
dart run build_runner build --delete-conflicting-outputs
test/bloc/home_bloc_test.dart
test('emits [Loading, Loaded] when fetch succeeds', () async {
  final mockApi = MockApiService();

  when(mockApi.fetchPosts).thenAnswer((_) async => [
    Post(id: 1, title: 'Hello'),
    Post(id: 2, title: 'World'),
  ]);

  final bloc = HomeBloc(apiService: mockApi);

  expectLater(
    bloc.stream,
    emitsInOrder([
      isA<HomeLoading>(),
      isA<HomeLoaded>().having(
        (s) => s.posts.length, 'post count', 2,
      ),
    ]),
  );

  bloc.add(LoadPosts());
});

Golden Tests (Visual Regression)

Golden tests compare rendered widgets against a stored reference image to catch visual regressions.

test/goldens/card_test.dart
testWidgets('product card matches golden', (WidgetTester tester) async {
  await tester.pumpWidget(
    MaterialApp(
      home: Scaffold(
        body: ProductCard(
          product: Product(
            name: 'Widget Pro',
            price: 29.99,
            imageUrl: 'assets/product.png',
          ),
        ),
      ),
    ),
  );

  await expectLater(
    find.byType(ProductCard),
    matchesGoldenFile('goldens/product_card.png'),
  );
});
# Regenerate all golden files
flutter test --update-goldens

TDD Workflow

Test-Driven Development follows the Red → Green → Refactor cycle:

🔄 Red → Green → Refactor
  1. Red: Write a failing test that defines the desired behavior
  2. Green: Write the minimum code to make the test pass
  3. Refactor: Clean up the code while keeping tests green
Step 1 — Write failing test
test('calculates total with discount', () {
  final cart = CartCalculator();
  cart.addItem(price: 100, quantity: 2);
  cart.applyDiscount(percent: 10);
  expect(cart.total, 180.0);
});
Step 2 — Make it pass
class CartCalculator {
  final List<CartItem> _items = [];
  double _discountPercent = 0;

  void addItem({required double price, required int quantity}) {
    _items.add(CartItem(price: price, quantity: quantity));
  }

  void applyDiscount({required double percent}) {
    _discountPercent = percent;
  }

  double get total {
    final subtotal = _items.fold<double>(
      0,
      (sum, item) => sum + item.price * item.quantity,
    );
    return subtotal * (1 - _discountPercent / 100);
  }
}

Summary

  • Write unit tests for all business logic and data models
  • Write widget tests to verify UI components render and respond to interactions
  • Write integration tests for critical user journeys
  • Use mockito to isolate dependencies
  • Use golden tests for visual regression detection
  • Follow TDD (Red → Green → Refactor) for new features
🚀 Next Step Continue to CI/CD & Deployment to automate your testing pipeline.
K
Kesavaraja Murugesan
Flutter Developer & Educator

Flutter developer with 3 years of experience building production-grade mobile applications. Passionate about teaching clean architecture patterns and helping developers write maintainable, scalable code.

← Animations Next: CI/CD & Deployment →