Testing Guide - utourismboard/explore-uganda-application-documentation GitHub Wiki

Testing Guide

Table of Contents

  1. Testing Overview
  2. Unit Testing
  3. Widget Testing
  4. Integration Testing
  5. Test Coverage
  6. CI/CD Integration

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

  1. Pre-commit Hooks

    # .git/hooks/pre-commit
    flutter test
    
  2. Continuous Testing

    • Run tests on every PR
    • Block merging if tests fail
    • Require code review on test changes

Best Practices

Testing Guidelines

  1. Test Organization

    • One test file per source file
    • Clear test descriptions
    • Proper test grouping
    • Shared test utilities
  2. Test Quality

    • Test edge cases
    • Handle async operations
    • Clean up after tests
    • Avoid test interdependence
  3. Performance

    • Mock heavy operations
    • Use setUp and tearDown
    • 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
    });
  });
}

Related Documentation