Documentation Index
Fetch the complete documentation index at: https://docs.agentsoflearning.com/llms.txt
Use this file to discover all available pages before exploring further.
Testing Guide
Overview
Comprehensive testing strategy for the AI-Powered Photo Journaling Flutter app covering unit tests, widget tests, integration tests, and manual testing procedures.
Testing Philosophy
- Test early, test often: Write tests alongside features
- Pyramid approach: More unit tests, fewer integration tests
- Test behavior, not implementation: Focus on what, not how
- Maintainable tests: Keep tests simple and clear
Test Coverage Goals
- Unit Tests: 80%+ coverage for business logic
- Widget Tests: 70%+ coverage for UI components
- Integration Tests: Critical user flows covered
- Manual Tests: All features tested on real devices
Running Tests
All Tests
# Run all tests
flutter test
# Run with coverage
flutter test --coverage
# Generate HTML coverage report
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
Specific Tests
# Run single test file
flutter test test/features/entry/entry_bloc_test.dart
# Run tests matching pattern
flutter test --name="EntryBloc"
# Run in watch mode (auto-rerun on changes)
flutter test --watch
Device Testing
# Run on connected iOS device
flutter test integration_test/app_test.dart
# Run with verbose logging
flutter test --verbose
Unit Testing
Testing Business Logic
File: test/features/entry/entry_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:app/features/entry/data/services/entry_service.dart';
void main() {
group('EntryService', () {
late EntryService service;
setUp(() {
service = EntryService();
});
tearDown(() {
// Clean up
});
test('should create entry with valid data', () async {
// Arrange
final entry = JournalEntry(
title: 'Test Entry',
content: 'Test content',
);
// Act
final result = await service.createEntry(entry);
// Assert
expect(result.id, isNotEmpty);
expect(result.title, equals('Test Entry'));
});
test('should throw ValidationException with empty title', () async {
// Arrange
final entry = JournalEntry(
title: '',
content: 'Test content',
);
// Act & Assert
expect(
() => service.createEntry(entry),
throwsA(isA<ValidationException>()),
);
});
});
}
Testing Utilities
File: test/core/utils/date_utils_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:app/core/utils/date_utils.dart';
void main() {
group('DateUtils', () {
test('should format date correctly', () {
final date = DateTime(2025, 11, 15);
final formatted = DateUtils.formatDate(date);
expect(formatted, equals('Nov 15, 2025'));
});
test('should calculate days ago', () {
final pastDate = DateTime.now().subtract(Duration(days: 5));
final daysAgo = DateUtils.daysAgo(pastDate);
expect(daysAgo, equals(5));
});
});
}
Testing UI Components
File: test/shared/widgets/buttons/primary_button_test.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:app/shared/widgets/buttons/primary_button.dart';
void main() {
testWidgets('PrimaryButton shows label', (WidgetTester tester) async {
// Build widget
await tester.pumpWidget(
CupertinoApp(
home: PrimaryButton(
label: 'Test Button',
onPressed: () {},
),
),
);
// Verify
expect(find.text('Test Button'), findsOneWidget);
});
testWidgets('PrimaryButton calls onPressed', (WidgetTester tester) async {
var pressed = false;
await tester.pumpWidget(
CupertinoApp(
home: PrimaryButton(
label: 'Test',
onPressed: () => pressed = true,
),
),
);
// Tap button
await tester.tap(find.text('Test'));
await tester.pump();
// Verify callback was called
expect(pressed, isTrue);
});
testWidgets('PrimaryButton is disabled when onPressed is null',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: PrimaryButton(
label: 'Test',
onPressed: null,
),
),
);
// Find button
final button = tester.widget<CupertinoButton>(
find.byType(CupertinoButton),
);
// Verify disabled
expect(button.enabled, isFalse);
});
}
Testing Screens
File: test/features/timeline/presentation/screens/timeline_screen_test.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:app/features/timeline/presentation/screens/timeline_screen.dart';
import 'package:app/features/timeline/presentation/bloc/timeline_bloc.dart';
void main() {
group('TimelineScreen', () {
late TimelineBloc bloc;
setUp(() {
bloc = TimelineBloc();
});
tearDown(() {
bloc.close();
});
testWidgets('shows loading indicator initially',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: BlocProvider.value(
value: bloc,
child: const TimelineScreen(),
),
),
);
expect(find.byType(CupertinoActivityIndicator), findsOneWidget);
});
testWidgets('shows empty state when no entries',
(WidgetTester tester) async {
// Emit empty state
bloc.add(const TimelineLoadRequested());
await tester.pumpWidget(
CupertinoApp(
home: BlocProvider.value(
value: bloc,
child: const TimelineScreen(),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(EmptyTimelineWidget), findsOneWidget);
});
});
}
BLoC Testing
File: test/features/entry/presentation/bloc/entry_bloc_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:app/features/entry/presentation/bloc/entry_bloc.dart';
void main() {
group('EntryBloc', () {
late EntryBloc bloc;
setUp(() {
bloc = EntryBloc();
});
tearDown(() {
bloc.close();
});
test('initial state is EntryInitial', () {
expect(bloc.state, equals(EntryInitial()));
});
blocTest<EntryBloc, EntryState>(
'emits [EntryLoading, EntryLoaded] when CreateEntryRequested is added',
build: () => bloc,
act: (bloc) => bloc.add(CreateEntryRequested(
title: 'Test',
content: 'Test content',
)),
expect: () => [
isA<EntryLoading>(),
isA<EntryLoaded>(),
],
);
blocTest<EntryBloc, EntryState>(
'emits [EntryLoading, EntryError] when creation fails',
build: () => bloc,
act: (bloc) => bloc.add(CreateEntryRequested(
title: '', // Invalid
content: 'Test',
)),
expect: () => [
isA<EntryLoading>(),
isA<EntryError>(),
],
);
});
}
Integration Testing
Setup
Create integration_test/app_test.dart:
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('App Integration Tests', () {
testWidgets('complete journal entry creation flow',
(WidgetTester tester) async {
// Start app
app.main();
await tester.pumpAndSettle();
// Navigate to create entry
await tester.tap(find.byIcon(CupertinoIcons.add));
await tester.pumpAndSettle();
// Capture photo (mock)
await tester.tap(find.text('Capture Photo'));
await tester.pumpAndSettle();
// Wait for AI prompt generation
await tester.pump(const Duration(seconds: 5));
await tester.pumpAndSettle();
// Select prompt
await tester.tap(find.text('Select this prompt'));
await tester.pumpAndSettle();
// Add journal text
await tester.enterText(
find.byType(CupertinoTextField),
'Test journal entry content',
);
// Save entry
await tester.tap(find.text('Save'));
await tester.pumpAndSettle();
// Verify entry appears in timeline
expect(find.text('Test journal entry content'), findsOneWidget);
});
});
}
Running Integration Tests
# Run integration tests
flutter test integration_test/app_test.dart
# Run on device
flutter test integration_test/app_test.dart --device-id=<device-id>
Manual Testing Checklist
Pre-Release Testing
Functional Testing
Accessibility Testing
Device Testing
Test on multiple devices:
Edge Cases
Mocking
Mock Services
Create mocks for external dependencies:
// test/mocks/mock_entry_service.dart
import 'package:mockito/annotations.dart';
import 'package:app/features/entry/data/services/entry_service.dart';
@GenerateMocks([EntryService])
void main() {}
Generate mocks:
flutter pub run build_runner build
Use in tests:
import 'package:mockito/mockito.dart';
import 'mocks/mock_entry_service.mocks.dart';
void main() {
test('should use mocked service', () {
final mockService = MockEntryService();
when(mockService.getEntries()).thenAnswer(
(_) async => [/* mock data */],
);
// Use mockService in test
});
}
Golden Tests
Test visual appearance:
testWidgets('EntryCard matches golden', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: EntryCard(entry: mockEntry),
),
);
await expectLater(
find.byType(EntryCard),
matchesGoldenFile('goldens/entry_card.png'),
);
});
Update goldens:
flutter test --update-goldens
CI/CD Integration
GitHub Actions (.github/workflows/test.yml):
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- name: Install dependencies
run: flutter pub get
- name: Run analyzer
run: flutter analyze
- name: Run tests
run: flutter test --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
Best Practices
-
Test Naming: Use descriptive names
- ✅
should create entry with valid data
- ❌
test1
-
Arrange-Act-Assert: Structure tests clearly
// Arrange: Setup
final input = 'test';
// Act: Execute
final result = function(input);
// Assert: Verify
expect(result, equals('expected'));
-
One Assertion Per Test: Keep tests focused
- Each test should verify one behavior
- Use multiple tests for multiple behaviors
-
Independent Tests: Tests should not depend on each other
- Each test should setup its own state
- Use
setUp() and tearDown()
-
Readable Tests: Write tests that document behavior
- Test names explain what is being tested
- Code is clean and easy to understand
Troubleshooting
Common Issues
Issue: Tests fail on CI but pass locally
- Solution: Ensure consistent Flutter version, check timezone dependencies
Issue: Widget tests fail with “No Material/Cupertino widget found”
- Solution: Wrap widget in
CupertinoApp or MaterialApp
Issue: Async tests timeout
- Solution: Increase timeout, check for infinite loops
Issue: Flaky tests (sometimes pass, sometimes fail)
- Solution: Add proper waits with
pumpAndSettle(), check for race conditions
Last Updated: 2025-11-15
Maintained By: QA Engineer / Frontend Developer