Tests - ApplETS/Notre-Dame GitHub Wiki

🧪 Testing et Assurance Qualité

Vue d'Ensemble

Les tests assurent la qualité, la stabilité et la maintenabilité du code.


1️⃣ Unit Tests

Test la logique métier isolée.

📦 Tests Repository (avec Mocks)

// test/data/repositories/course_repository_test.dart
import 'package:mockito/mockito.dart';
import 'package:notredame/data/repositories/course_repository.dart';
import 'package:notredame/data/services/signets_client_service.dart';

// Générer les mocks (voir pubspec.yaml avec build_runner)
// flutter pub run build_runner build

void main() {
  group('CourseRepository', () {
    late MockSignetsClientService mockSignetsClient;
    late CourseRepository repository;
    
    setUp(() {
      mockSignetsClient = MockSignetsClientService();
      repository = CourseRepository(
        signetsClient: mockSignetsClient,
      );
    });
    
    test('getCourses throws exception on network error', () async {
      // Arrange
      when(mockSignetsClient.getCourses('A2024'))
          .thenThrow(NetworkException('Network error'));
      
      // Act & Assert
      expect(
        () => repository.getCourses('A2024'),
        throwsA(isA<RepositoryException>()),
      );
    });
    
    test('getCourses caches results', () async {
      // Arrange
      final mockCourses = [/* ... */];
      when(mockSignetsClient.getCourses('A2024'))
          .thenAnswer((_) async => mockCourses);
      
      // Act
      await repository.getCourses('A2024');
      await repository.getCourses('A2024');  // Deuxième appel
      
      // Assert - le service ne doit être appelé qu'une fois
      verify(mockSignetsClient.getCourses('A2024')).called(1);
    });
  });
}

2️⃣ Widget Tests

Test les interfaces utilisateur (Widgets).

Basic Widget Test

// test/ui/schedule/widgets/schedule_view_test.dart
void main() {
  group('ScheduleView', () {
    testWidgets('displays loading indicator while loading', (tester) async {
      // Arrange
      final mockViewModel = MockScheduleViewModel();
      when(mockViewModel.isLoading).thenReturn(true);
      
      // Act
      await tester.pumpWidget(
        MaterialApp(
          home: ChangeNotifierProvider(
            create: (_) => mockViewModel,
            child: const ScheduleView(),
          ),
        ),
      );
      
      // Assert
      expect(find.byType(CircularProgressIndicator), findsOneWidget);
    });
    
    testWidgets('displays courses list when data loaded', (tester) async {
      // Arrange
      final mockViewModel = MockScheduleViewModel();
      when(mockViewModel.isLoading).thenReturn(false);
      when(mockViewModel.activities).thenReturn([
        CourseActivity(/* ... */),
        CourseActivity(/* ... */),
      ]);
      
      // Act
      await tester.pumpWidget(
        MaterialApp(
          home: ChangeNotifierProvider(
            create: (_) => mockViewModel,
            child: const ScheduleView(),
          ),
        ),
      );
      
      // Assert
      expect(find.byType(ListView), findsOneWidget);
      expect(find.byType(CalendarEventTile), findsWidgets);
    });
    
    testWidgets('displays error message on error', (tester) async {
      // Arrange
      final mockViewModel = MockScheduleViewModel();
      when(mockViewModel.isLoading).thenReturn(false);
      when(mockViewModel.error).thenReturn('Failed to load');
      
      // Act
      await tester.pumpWidget(
        MaterialApp(
          home: ChangeNotifierProvider(
            create: (_) => mockViewModel,
            child: const ScheduleView(),
          ),
        ),
      );
      
      // Assert
      expect(find.byType(ErrorWidget), findsOneWidget);
    });
    
    testWidgets('calls loadSchedule on initState', (tester) async {
      // Arrange
      final mockViewModel = MockScheduleViewModel();
      
      // Act
      await tester.pumpWidget(
        MaterialApp(
          home: ChangeNotifierProvider(
            create: (_) => mockViewModel,
            child: const ScheduleView(),
          ),
        ),
      );
      
      await tester.pumpAndSettle();
      
      // Assert
      verify(mockViewModel.loadSchedule()).called(1);
    });
  });
}

Gesture Testing

testWidgets('navigates to details on course tap', (tester) async {
  // Arrange
  final course = CourseActivity(/* ... */);
  
  await tester.pumpWidget(
    MaterialApp(
      home: Scaffold(
        body: CalendarEventTile(activity: course),
      ),
    ),
  );
  
  // Act
  await tester.tap(find.byType(CalendarEventTile));
  await tester.pumpAndSettle();
  
  // Assert - verifier la navigation ou le changement d'UI
});

🔧 Mockito - Mocking Framework

Générer les Mocks

# Générer les mocks
flutter pub run build_runner build

Créer des Mocks

// test/mocks/mock_repositories.dart
import 'package:mockito/mockito.dart';

// Les mocks sont générés automatiquement
@GenerateNiceMocks([MockSpec<CourseRepository>()])
class CourseRepositoryMock extends MockCourseRepository {}

@GenerateNiceMocks([MockSpec<AuthService>()])
class AuthServiceMock extends MockAuthService {}

// Utilisation
final mockRepository = MockCourseRepository();
when(mockRepository.getCourses(any))
    .thenAnswer((_) async => [/* ... */]);

Mock Stubs

// Configurer le comportement du mock
when(mockService.getUser())
    .thenAnswer((_) async => User(id: '1', name: 'John'));

when(mockService.logout())
    .thenThrow(AuthException('Network error'));

when(mockService.isOnline())
    .thenReturn(true);

Vérifier les Appels

// Vérifier qu'une méthode a été appelée
verify(mockService.getUser()).called(1);

// Vérifier qu'elle n'a pas été appelée
verifyNever(mockService.logout());

// Vérifier l'ordre des appels
verifyInOrder([
  mockService.getUser(),
  mockService.getCourses(),
]);

📊 Coverage & Rapports

Exécuter les Tests avec Coverage

# Générer un rapport de couverture
flutter test --coverage

# Générer un rapport HTML
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html

Cible de Couverture

  • Idéal : 80% de couverture globale
  • Accepté : 70% minimum
  • Domain : 90%+
  • Data : 80%+
  • UI : 60%+

Fichier de Configuration

# .gitignore
coverage/

🧪 Exécuter les Tests

Tests Unitaires

# Tous les tests
flutter test

# Tests spécifiques
flutter test test/domain/
flutter test test/data/repositories/

# Avec verbose
flutter test --verbose

# Reporter json
flutter test --reporter json > test-results.json

Tests Widget

# Tests widget uniquement
flutter test --tags=widget

# Avec driver
flutter drive --target=test_driver/app.dart

Tests en Couverture

# Afficher les fichiers non testés
flutter test --coverage

# Générer un rapport HTML
# (Utiliser le package: coverage)
pub run coverage:format_coverage --packages=.packages

🎯 Bonnes Pratiques de Test

✅ À FAIRE

// ✅ Tester une seule chose par test
test('returns list when data is valid', () async {
  final result = await repository.getItems();
  expect(result, isNotEmpty);
});

// ✅ Nommer clairement
test('getCourses throws RepositoryException on network error', () {
  // ...
});

// ✅ Utiliser setUp et tearDown
setUp(() {
  mockRepository = MockCourseRepository();
});

tearDown(() {
  mockRepository = null;
});

// ✅ Vérifier les mocks appelés
verify(mockService.getUser()).called(1);

// ✅ Utiliser matchers expressifs
expect(courses, isNotEmpty);
expect(error, isInstanceOf<NetworkException>());

❌ À ÉVITER

// ❌ Tester trop de choses
test('complete user flow from login to logout', () {
  // 500 lignes...
});

// ❌ Nom vague
test('test repository', () { /* ... */ });

// ❌ Tests dépendants les uns des autres
test('1. login user', () { /* ... */ });
test('2. get courses', () { /* depend du test 1 */ });

// ❌ Pas de setup/teardown
void main() {
  final mockService = MockService();  // Partagé entre tests
  test('test 1', () { /* use mockService */ });
  test('test 2', () { /* pollué par test 1 */ });
}

📚 Ressources de Test


✅ Checklist de Test pour Nouvelle Feature

  • Tests unitaires pour les modèles (domain)
  • Tests des repositories avec mocks
  • Tests des viewmodels
  • Tests basiques des widgets (loading, error, data)
  • Tests de l'interaction utilisateur (taps, inputs)
  • Coverage > 70%
  • Tous les tests passent
  • Code lintless (flutter analyze)
  • Pas de warnings

Cette page a été en partie générée avec l'aide de Claude Haiku 4.5