Flutter Testing
This document outlines the testing strategy and implementation for the BookWish Flutter mobile application.
Testing Framework
Dependencies
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
riverpod_generator: ^2.3.0
build_runner: ^2.4.0
Test Directory Structure
app/
├── lib/
│ ├── models/
│ ├── services/
│ ├── providers/
│ ├── ui/
│ └── utils/
└── test/
├── widget_test.dart # Basic smoke tests
├── models/ # Model unit tests
├── services/ # Service unit tests
├── providers/ # Provider tests
└── widgets/ # Widget tests
Test Types
1. Unit Tests
Unit tests verify individual functions, methods, and classes in isolation.
Model Tests
import 'package:flutter_test/flutter_test.dart';
import 'package:bookwish/models/book.dart';
void main() {
group('Book Model', () {
test('fromJson creates valid Book instance', () {
final json = {
'id': '123',
'title': 'Test Book',
'authors': ['Author Name'],
'isbn13': '9781234567890',
};
final book = Book.fromJson(json);
expect(book.id, '123');
expect(book.title, 'Test Book');
expect(book.authors, ['Author Name']);
expect(book.isbn13, '9781234567890');
});
test('toJson serializes correctly', () {
final book = Book(
id: '123',
title: 'Test Book',
authors: ['Author Name'],
);
final json = book.toJson();
expect(json['id'], '123');
expect(json['title'], 'Test Book');
expect(json['authors'], ['Author Name']);
});
});
}
Service Tests
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:bookwish/services/api_service.dart';
void main() {
group('ApiService', () {
late ApiService apiService;
late MockHttpClient mockHttpClient;
setUp(() {
mockHttpClient = MockHttpClient();
apiService = ApiService(httpClient: mockHttpClient);
});
test('fetchBooks returns list of books on success', () async {
// Arrange
final mockResponse = {
'books': [
{'id': '1', 'title': 'Book 1'},
{'id': '2', 'title': 'Book 2'},
]
};
when(mockHttpClient.get(any))
.thenAnswer((_) async => Response(mockResponse, 200));
// Act
final books = await apiService.fetchBooks();
// Assert
expect(books.length, 2);
expect(books[0].title, 'Book 1');
});
});
}
2. Widget Tests
Widget tests verify UI components and user interactions.
Basic Widget Test
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:bookwish/ui/components/book_card.dart';
void main() {
testWidgets('BookCard displays book information', (WidgetTester tester) async {
// Arrange
final book = Book(
id: '1',
title: 'Test Book',
authors: ['Author Name'],
coverImageUrl: 'https://example.com/cover.jpg',
);
// Act
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BookCard(book: book),
),
),
);
// Assert
expect(find.text('Test Book'), findsOneWidget);
expect(find.text('Author Name'), findsOneWidget);
});
testWidgets('BookCard handles tap events', (WidgetTester tester) async {
bool tapped = false;
final book = Book(id: '1', title: 'Test Book');
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BookCard(
book: book,
onTap: () => tapped = true,
),
),
),
);
// Tap the card
await tester.tap(find.byType(BookCard));
await tester.pump();
expect(tapped, true);
});
}
Testing with Riverpod
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Widget updates when provider changes', (tester) async {
final container = ProviderContainer();
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: MaterialApp(
home: MyWidget(),
),
),
);
// Verify initial state
expect(find.text('Loading'), findsOneWidget);
// Update provider state
container.read(myProvider.notifier).updateState();
await tester.pump();
// Verify updated state
expect(find.text('Loaded'), findsOneWidget);
});
}
3. Integration Tests
Integration tests verify complete user flows across multiple screens.
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:bookwish/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('BookWish App Integration Tests', () {
testWidgets('User can search for books and add to wishlist',
(WidgetTester tester) async {
// Launch app
app.main();
await tester.pumpAndSettle();
// Navigate to search
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
// Enter search query
await tester.enterText(find.byType(TextField), 'Harry Potter');
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
// Tap first book result
await tester.tap(find.byType(BookCard).first);
await tester.pumpAndSettle();
// Add to wishlist
await tester.tap(find.text('Add to Wishlist'));
await tester.pumpAndSettle();
// Verify success message
expect(find.text('Added to wishlist'), findsOneWidget);
});
});
}
Mocking
Platform Channels
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const MethodChannel channel = MethodChannel('bookwish/camera');
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == 'scanBarcode') {
return '9781234567890'; // Mock ISBN
}
return null;
});
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null);
});
test('Scanner returns ISBN', () async {
final isbn = await channel.invokeMethod('scanBarcode');
expect(isbn, '9781234567890');
});
}
Firebase
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart';
import 'package:flutter_test/flutter_test.dart';
void setupFirebaseAuthMocks() {
TestWidgetsFlutterBinding.ensureInitialized();
setupFirebaseCoreMocks();
}
Future<void> initializeFirebaseForTests() async {
await Firebase.initializeApp();
}
void main() {
setupFirebaseAuthMocks();
setUpAll(() async {
await initializeFirebaseForTests();
});
// Your tests here
}
HTTP Requests
import 'package:http/http.dart' as http;
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
([http.Client])
import 'api_test.mocks.dart';
void main() {
group('API Tests', () {
late MockClient mockClient;
setUp(() {
mockClient = MockClient();
});
test('Fetch book returns Book on success', () async {
when(mockClient.get(any))
.thenAnswer((_) async => http.Response('{"id": "1", "title": "Book"}', 200));
final book = await apiService.fetchBook('1');
expect(book.title, 'Book');
verify(mockClient.get(any)).called(1);
});
});
}
Test Utilities
Golden Tests (Visual Regression)
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
void main() {
testGoldens('BookCard matches golden', (tester) async {
final builder = GoldenBuilder.grid(
columns: 2,
widthToHeightRatio: 1,
)
..addScenario('Default', BookCard(book: sampleBook))
..addScenario('Used Book', BookCard(book: usedBook))
..addScenario('No Cover', BookCard(book: noCoverBook));
await tester.pumpWidgetBuilder(builder.build());
await screenMatchesGolden(tester, 'book_card_grid');
});
}
Test Helpers
// test/helpers/test_helpers.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
/// Wraps a widget in MaterialApp for testing
Widget wrapWithMaterialApp(Widget child) {
return MaterialApp(
home: Scaffold(body: child),
);
}
/// Pumps widget and waits for all animations
Future<void> pumpAndSettle(WidgetTester tester, Widget widget) async {
await tester.pumpWidget(widget);
await tester.pumpAndSettle();
}
/// Common test books
final testBook = Book(
id: '1',
title: 'Test Book',
authors: ['Test Author'],
);
final usedBook = Book(
id: '2',
title: 'Used Book',
isUsed: true,
condition: 'good',
);
Running Tests
Run All Tests
flutter test
Run Specific Test File
flutter test test/models/book_test.dart
Run with Coverage
flutter test --coverage
View Coverage Report
# Generate lcov report
genhtml coverage/lcov.info -o coverage/html
# Open in browser
open coverage/html/index.html
Run Integration Tests
flutter test integration_test/
CI/CD Integration
GitHub Actions Example
name: Flutter Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- run: flutter pub get
- run: flutter analyze
- run: flutter test --coverage
- uses: codecov/codecov-action@v2
with:
files: ./coverage/lcov.info
Current Test Status
The BookWish Flutter app currently has:
- Basic smoke test in
test/widget_test.dart - Limited coverage due to platform plugin dependencies (Firebase, push notifications)
- No integration tests yet implemented
Challenges
- Platform Dependencies: Firebase and push notifications require platform-specific setup
- Mock Complexity: Extensive mocking needed for Firebase Auth, Firestore, FCM
- State Management: Riverpod testing requires careful provider scope management
Best Practices
1. Test Organization
- Group related tests with
group() - Use descriptive test names
- Follow Arrange-Act-Assert pattern
2. Naming Conventions
test('should return user when authentication succeeds', () {});
testWidgets('LoginPage displays error on invalid credentials', () {});
3. Avoid Test Interdependence
- Each test should be independent
- Use
setUp()andtearDown()for common setup/cleanup - Don't rely on test execution order
4. Mock External Dependencies
- Mock all network calls
- Mock platform channels
- Mock Firebase services
5. Keep Tests Fast
- Minimize widget pump operations
- Use unit tests for business logic
- Reserve widget/integration tests for UI flows
Coverage Goals
- Unit Tests: 80%+ coverage for models, services, utilities
- Widget Tests: Coverage for all reusable components
- Integration Tests: Critical user flows (auth, book search, wishlist)
Future Improvements
-
Increase Unit Test Coverage
- Add tests for all models
- Test all service methods
- Cover edge cases and error handling
-
Widget Test Suite
- Test all custom widgets
- Verify state changes
- Test user interactions
-
Integration Tests
- Complete user flows
- Authentication flows
- Book discovery and purchase
- Wishlist management
-
Golden Tests
- Visual regression testing for components
- Multiple device sizes
- Light/dark mode variants
-
Performance Tests
- Frame rendering times
- Memory usage
- Network request optimization