Skip to main content

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

  1. Platform Dependencies: Firebase and push notifications require platform-specific setup
  2. Mock Complexity: Extensive mocking needed for Firebase Auth, Firestore, FCM
  3. 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() and tearDown() 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

  1. Increase Unit Test Coverage

    • Add tests for all models
    • Test all service methods
    • Cover edge cases and error handling
  2. Widget Test Suite

    • Test all custom widgets
    • Verify state changes
    • Test user interactions
  3. Integration Tests

    • Complete user flows
    • Authentication flows
    • Book discovery and purchase
    • Wishlist management
  4. Golden Tests

    • Visual regression testing for components
    • Multiple device sizes
    • Light/dark mode variants
  5. Performance Tests

    • Frame rendering times
    • Memory usage
    • Network request optimization