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