Testing Guide - utourismboard/explore-uganda-application-documentation GitHub Wiki
Testing Guide
Table of Contents
Testing Overview
Testing Structure
test/
├── unit/
│ ├── repositories/
│ ├── services/
│ └── utils/
├── widget/
│ ├── screens/
│ └── components/
└── integration/
├── flows/
└── e2e/
Test Dependencies
# pubspec.yaml
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
mockito: ^5.4.4
bloc_test: ^9.1.5
golden_toolkit: ^0.15.0
Unit Testing
Writing Unit Tests
// Example unit test for TouristSite repository
void main() {
late MockTouristSiteApi mockApi;
late TouristSiteRepository repository;
setUp(() {
mockApi = MockTouristSiteApi();
repository = TouristSiteRepository(api: mockApi);
});
group('TouristSiteRepository', () {
test('getAllSites returns list of tourist sites', () async {
// Arrange
when(mockApi.getSites()).thenAnswer(
(_) async => [
{'id': '1', 'name': 'Site 1'},
{'id': '2', 'name': 'Site 2'},
],
);
// Act
final result = await repository.getAllSites();
// Assert
expect(result.length, 2);
expect(result[0].id, '1');
expect(result[0].name, 'Site 1');
verify(mockApi.getSites()).called(1);
});
test('getSiteById throws exception when site not found', () {
// Arrange
when(mockApi.getSiteById('999')).thenThrow(NotFoundException());
// Act & Assert
expect(
() => repository.getSiteById('999'),
throwsA(isA<NotFoundException>()),
);
});
});
}
Mocking
// Example mock using Mockito
@GenerateMocks([TouristSiteApi])
class MockTouristSiteApi extends Mock implements TouristSiteApi {}
// Example mock response
final mockSiteResponse = {
'id': '1',
'name': 'Murchison Falls',
'description': 'Beautiful waterfall',
'location': {
'latitude': 2.2748,
'longitude': 31.6799,
},
'images': ['image1.jpg', 'image2.jpg'],
};
Widget Testing
Widget Test Example
void main() {
group('TouristSiteScreen', () {
testWidgets('displays loading indicator when loading',
(WidgetTester tester) async {
// Arrange
final mockProvider = MockTouristSiteProvider();
when(mockProvider.isLoading).thenReturn(true);
// Act
await tester.pumpWidget(
MaterialApp(
home: ChangeNotifierProvider<TouristSiteProvider>.value(
value: mockProvider,
child: TouristSiteScreen(siteId: '1'),
),
),
);
// Assert
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('displays error message on error',
(WidgetTester tester) async {
// Test implementation
});
});
}
Golden Tests
void main() {
group('TouristSiteCard Golden Tests', () {
testGoldens('matches golden file', (tester) async {
// Arrange
final builder = DeviceBuilder()
..addScenario(
widget: TouristSiteCard(
site: TouristSite(
id: '1',
name: 'Test Site',
description: 'Test Description',
images: ['test.jpg'],
),
),
);
// Act & Assert
await tester.pumpDeviceBuilder(builder);
await screenMatchesGolden(tester, 'tourist_site_card');
});
});
}
Integration Testing
Integration Test Example
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('end-to-end test', () {
testWidgets('complete booking flow', (tester) async {
// Launch app
app.main();
await tester.pumpAndSettle();
// Navigate to tourist site
await tester.tap(find.byKey(Key('site_card_1')));
await tester.pumpAndSettle();
// Fill booking details
await tester.enterText(
find.byKey(Key('date_picker')),
'2024-04-01',
);
await tester.tap(find.byKey(Key('book_button')));
await tester.pumpAndSettle();
// Verify booking confirmation
expect(find.text('Booking Confirmed'), findsOneWidget);
});
});
}
Test Coverage
Running Tests with Coverage
# Run all tests with coverage
flutter test --coverage
# Generate coverage report
genhtml coverage/lcov.info -o coverage/html
# Open coverage report
open coverage/html/index.html
Coverage Requirements
- Minimum coverage: 80%
- Critical paths: 90%
- UI components: 70%
- Utility functions: 95%
CI/CD Integration
GitHub Actions Example
name: Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.19.0'
- name: Install dependencies
run: flutter pub get
- name: Run tests
run: flutter test --coverage
- name: Upload coverage
uses: codecov/codecov-action@v2
with:
file: coverage/lcov.info
Test Automation
-
Pre-commit Hooks
# .git/hooks/pre-commit flutter test
-
Continuous Testing
- Run tests on every PR
- Block merging if tests fail
- Require code review on test changes
Best Practices
Testing Guidelines
-
Test Organization
- One test file per source file
- Clear test descriptions
- Proper test grouping
- Shared test utilities
-
Test Quality
- Test edge cases
- Handle async operations
- Clean up after tests
- Avoid test interdependence
-
Performance
- Mock heavy operations
- Use
setUp
andtearDown
- Group similar tests
- Optimize test execution
Common Patterns
// Example test pattern
void main() {
late MockDependency mock;
late SystemUnderTest sut;
setUp(() {
mock = MockDependency();
sut = SystemUnderTest(mock);
});
tearDown(() {
// Cleanup
});
group('feature test', () {
test('happy path', () {
// Arrange
when(mock.someMethod()).thenReturn(value);
// Act
final result = sut.someMethod();
// Assert
expect(result, expectedValue);
verify(mock.someMethod()).called(1);
});
test('error path', () {
// Test implementation
});
});
}