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.
- 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
dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^5.4.0
build_runner: ^2.4.0
Writing Your First Unit Test
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('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>()),
);
});
expect(value, equals(expected))— exact equalityexpect(value, isNull)/isNotNullexpect(value, isA<Type>())— type checkingexpect(list, contains(item))— collection matchersexpect(fn, throwsA(matcher))— exception matching
Widget Tests
Widget tests run in a simulated Flutter environment and verify that widgets render and interact correctly.
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() 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.
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.
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('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.
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: Write a failing test that defines the desired behavior
- Green: Write the minimum code to make the test pass
- Refactor: Clean up the code while keeping tests green
test('calculates total with discount', () {
final cart = CartCalculator();
cart.addItem(price: 100, quantity: 2);
cart.applyDiscount(percent: 10);
expect(cart.total, 180.0);
});
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