Tests
Introduction
Nylo Website v7 inclut un framework de test complet inspire des utilitaires de test de Laravel. Il fournit :
- Fonctions de test avec setup/teardown automatiques (
nyTest,nyWidgetTest,nyGroup) - Simulation d'authentification via
NyTest.actingAs<T>() - Voyage dans le temps pour figer ou manipuler le temps dans les tests
- Simulation d'API avec correspondance de patrons d'URL et suivi des appels
- Factories avec un generateur de donnees fictives integre (
NyFaker) - Simulation de canaux de plateforme pour le stockage securise, le fournisseur de chemins et plus
- Assertions personnalisees pour les routes, Backpack, l'authentification et l'environnement
Pour commencer
Initialisez le framework de test en haut de votre fichier de test :
import 'package:nylo_framework/nylo_framework.dart';
void main() {
NyTest.init();
nyTest('my first test', () async {
expect(1 + 1, equals(2));
});
}
NyTest.init() configure l'environnement de test et active la reinitialisation automatique de l'etat entre les tests lorsque autoReset: true (valeur par defaut).
Ecrire des tests
nyTest
La fonction principale pour ecrire des tests :
nyTest('can save and read from storage', () async {
backpackSave("name", "Anthony");
expect(backpackRead<String>("name"), equals("Anthony"));
});
Options :
nyTest('my test', () async {
// test body
}, skip: false, timeout: Timeout(Duration(seconds: 30)));
nyWidgetTest
Pour tester les widgets Flutter avec un WidgetTester :
nyWidgetTest('renders a button', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: ElevatedButton(
onPressed: () {},
child: Text("Tap me"),
),
),
));
expect(find.text("Tap me"), findsOneWidget);
});
Utilitaires de test de widgets
La classe NyWidgetTest et les extensions WidgetTester fournissent des aides pour injecter des widgets Nylo avec un support de theme correct, attendre la fin de init(), et tester les etats de chargement.
Configurer l'environnement de test
Appelez NyWidgetTest.configure() dans votre setUpAll pour desactiver la recuperation de Google Fonts et optionnellement definir un theme personnalise :
nySetUpAll(() async {
NyWidgetTest.configure(testTheme: ThemeData.light());
await setupApplication(providers);
});
Vous pouvez reinitialiser la configuration avec NyWidgetTest.reset().
Deux themes integres sont disponibles pour les tests sans polices :
ThemeData light = NyWidgetTest.simpleTestTheme;
ThemeData dark = NyWidgetTest.simpleDarkTestTheme;
Injecter des widgets Nylo
Utilisez pumpNyWidget pour envelopper un widget dans un MaterialApp avec support de theme :
nyWidgetTest('renders page', (tester) async {
await tester.pumpNyWidget(
HomePage(),
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.light,
settleTimeout: Duration(seconds: 5),
useSimpleTheme: false,
);
expect(find.text('Welcome'), findsOneWidget);
});
Pour une injection rapide avec un theme sans polices :
await tester.pumpNyWidgetSimple(HomePage());
Attendre l'initialisation
pumpNyWidgetAndWaitForInit injecte des frames jusqu'a ce que les indicateurs de chargement disparaissent (ou que le delai soit atteint), ce qui est utile pour les pages avec des methodes init() asynchrones :
await tester.pumpNyWidgetAndWaitForInit(
HomePage(),
timeout: Duration(seconds: 10),
useSimpleTheme: true,
);
// init() has completed
expect(find.text('Loaded Data'), findsOneWidget);
Aides de pompage
// Pump frames until a specific widget appears
bool found = await tester.pumpUntilFound(
find.text('Welcome'),
timeout: Duration(seconds: 5),
);
// Settle gracefully (won't throw on timeout)
await tester.pumpAndSettleGracefully(timeout: Duration(seconds: 5));
Simulation de cycle de vie
Simulez des changements de AppLifecycleState sur n'importe quel NyPage dans l'arbre de widgets :
await tester.pumpNyWidget(MyPage());
await tester.simulateLifecycleState(AppLifecycleState.paused);
await tester.pump();
// Assert side effects of the paused lifecycle action
Verifications de chargement et de verrouillage
Verifiez les cles de chargement nommees et les verrous sur les widgets NyPage/NyState :
// Check if a named loading key is active
bool loading = tester.isLoadingNamed(find.byType(MyPage), name: 'fetchUsers');
// Check if a named lock is held
bool locked = tester.isLockedNamed(find.byType(MyPage), name: 'submit');
// Check for any loading indicator (CircularProgressIndicator or Skeletonizer)
bool isAnyLoading = tester.isLoading();
Aide testNyPage
Une fonction pratique qui injecte un NyPage, attend l'initialisation, puis execute vos attentes :
testNyPage(
'HomePage loads correctly',
build: () => HomePage(),
expectations: (tester) async {
expect(find.text('Welcome'), findsOneWidget);
},
useSimpleTheme: true,
initTimeout: Duration(seconds: 10),
skip: false,
);
Aide testNyPageLoading
Testez qu'une page affiche un indicateur de chargement pendant init() :
testNyPageLoading(
'HomePage shows loading state',
build: () => HomePage(),
skip: false,
);
NyPageTestMixin
Un mixin fournissant des utilitaires de test de page courants :
class HomePageTest with NyPageTestMixin {
void runTests(WidgetTester tester) async {
// Verify init was called and loading completed
await verifyInitCalled(tester, HomePage(), timeout: Duration(seconds: 5));
// Verify loading state is shown during init
await verifyLoadingState(tester, HomePage());
}
}
nyGroup
Regroupez des tests lies ensemble :
nyGroup('Authentication', () {
nyTest('can login', () async {
NyTest.actingAs<User>(User(name: "Anthony"));
expectAuthenticated<User>();
});
nyTest('can logout', () async {
NyTest.actingAs<User>(User(name: "Anthony"));
NyTest.logout();
expectGuest();
});
});
Cycle de vie des tests
Configurez la logique de setup et teardown a l'aide de hooks de cycle de vie :
void main() {
NyTest.init();
nySetUpAll(() {
// Runs once before all tests
});
nySetUp(() {
// Runs before each test
});
nyTearDown(() {
// Runs after each test
});
nyTearDownAll(() {
// Runs once after all tests
});
}
Ignorer des tests et tests CI
// Skip a test with a reason
nySkip('not implemented yet', () async {
// ...
}, "Waiting for API update");
// Tests expected to fail
nyFailing('known bug', () async {
// ...
});
// CI-only tests (tagged with 'ci')
nyCi('integration test', () async {
// Only runs in CI environments
});
Authentification
Simulez des utilisateurs authentifies dans les tests :
nyTest('user can access profile', () async {
// Simulate a logged-in user
NyTest.actingAs<User>(User(name: "Anthony", email: "anthony@example.com"));
// Verify authenticated
expectAuthenticated<User>();
// Access the acting user
User? user = NyTest.actingUser<User>();
expect(user?.name, equals("Anthony"));
});
nyTest('guest cannot access profile', () async {
// Verify not authenticated
expectGuest();
});
Deconnectez l'utilisateur :
NyTest.logout();
expectGuest();
Voyage dans le temps
Manipulez le temps dans vos tests en utilisant NyTime :
Se deplacer a une date specifique
nyTest('time travel to 2025', () async {
NyTest.travel(DateTime(2025, 1, 1));
expect(NyTime.now().year, equals(2025));
NyTest.travelBack(); // Reset to real time
});
Avancer ou reculer dans le temps
NyTest.travelForward(Duration(days: 30)); // Jump 30 days ahead
NyTest.travelBackward(Duration(hours: 2)); // Go back 2 hours
Figer le temps
NyTest.freezeTime(); // Freeze at the current moment
DateTime frozen = NyTime.now();
await Future.delayed(Duration(seconds: 1));
expect(NyTime.now(), equals(frozen)); // Time hasn't moved
NyTest.travelBack(); // Unfreeze
Limites temporelles
NyTime.travelToStartOfDay(); // 00:00:00.000
NyTime.travelToEndOfDay(); // 23:59:59.999
NyTime.travelToStartOfMonth(); // 1st of current month
NyTime.travelToEndOfMonth(); // Last day of current month
NyTime.travelToStartOfYear(); // Jan 1st
NyTime.travelToEndOfYear(); // Dec 31st
Voyage dans le temps avec portee
Executez du code dans un contexte temporel fige :
await NyTime.withFrozenTime<void>(DateTime(2025, 6, 15), () async {
expect(NyTime.now(), equals(DateTime(2025, 6, 15)));
});
// Time is automatically restored after the callback
Simulation d'API
Simulation par patron d'URL
Simulez des reponses API en utilisant des patrons d'URL avec prise en charge des caracteres generiques :
nyTest('mock API responses', () async {
// Exact URL match
NyMockApi.respond('/users/1', {'id': 1, 'name': 'Anthony'});
// Single segment wildcard (*)
NyMockApi.respond('/users/*', {'id': 1, 'name': 'User'});
// Multi-segment wildcard (**)
NyMockApi.respond('/api/**', {'status': 'ok'});
// With status code and headers
NyMockApi.respond(
'/users',
{'error': 'Unauthorized'},
statusCode: 401,
method: 'POST',
headers: {'X-Error': 'true'},
);
// With simulated delay
NyMockApi.respond(
'/slow-endpoint',
{'data': 'loaded'},
delay: Duration(seconds: 2),
);
});
Simulation par type de service API
Simulez un service API entier par type :
nyTest('mock API service', () async {
NyMockApi.register<UserApiService>((MockApiRequest request) async {
if (request.endpoint.contains('/users')) {
return {'users': [{'id': 1, 'name': 'Anthony'}]};
}
return {'error': 'not found'};
});
});
Historique des appels et assertions
Suivez et verifiez les appels API :
nyTest('verify API was called', () async {
NyMockApi.setRecordCalls(true);
// ... perform actions that trigger API calls ...
// Assert endpoint was called
expectApiCalled('/users');
// Assert endpoint was not called
expectApiNotCalled('/admin');
// Assert call count
expectApiCalled('/users', times: 2);
// Assert specific method
expectApiCalled('/users', method: 'POST');
// Get call details
List<ApiCallInfo> calls = NyMockApi.getCallsFor('/users');
});
Creer des reponses simulees
Response<Map<String, dynamic>> response = NyMockApi.createResponse(
data: {'id': 1, 'name': 'Anthony'},
statusCode: 200,
statusMessage: 'OK',
);
Factories
Definir des factories
Definissez comment creer des instances de test de vos modeles :
NyFactory.define<User>((NyFaker faker) => User(
name: faker.name(),
email: faker.email(),
phone: faker.phone(),
));
Avec prise en charge des surcharges :
NyFactory.defineWithOverrides<User>((NyFaker faker, Map<String, dynamic> attributes) => User(
name: attributes['name'] ?? faker.name(),
email: attributes['email'] ?? faker.email(),
phone: attributes['phone'] ?? faker.phone(),
));
Etats de factory
Definissez des variations d'une factory :
NyFactory.state<User>('admin', (User user, NyFaker faker) {
return User(name: user.name, email: user.email, role: 'admin');
});
NyFactory.state<User>('premium', (User user, NyFaker faker) {
return User(name: user.name, email: user.email, subscription: 'premium');
});
Creer des instances
// Create a single instance
User user = NyFactory.make<User>();
// Create with overrides
User admin = NyFactory.make<User>(overrides: {'name': 'Admin User'});
// Create with states applied
User premiumAdmin = NyFactory.make<User>(states: ['admin', 'premium']);
// Create multiple instances
List<User> users = NyFactory.create<User>(count: 5);
// Create a sequence with index-based data
List<User> numbered = NyFactory.sequence<User>(3, (int index, NyFaker faker) {
return User(name: "User ${index + 1}", email: faker.email());
});
NyFaker
NyFaker genere des donnees fictives realistes pour les tests. Il est disponible dans les definitions de factory et peut etre instancie directement.
NyFaker faker = NyFaker();
Methodes disponibles
| Categorie | Methode | Type de retour | Description |
|---|---|---|---|
| Noms | faker.firstName() |
String |
Prenom aleatoire |
faker.lastName() |
String |
Nom de famille aleatoire | |
faker.name() |
String |
Nom complet (prenom + nom) | |
faker.username() |
String |
Nom d'utilisateur | |
| Contact | faker.email() |
String |
Adresse e-mail |
faker.phone() |
String |
Numero de telephone | |
faker.company() |
String |
Nom d'entreprise | |
| Nombres | faker.randomInt(min, max) |
int |
Entier aleatoire dans une plage |
faker.randomDouble(min, max) |
double |
Double aleatoire dans une plage | |
faker.randomBool() |
bool |
Booleen aleatoire | |
| Identifiants | faker.uuid() |
String |
Chaine UUID v4 |
| Dates | faker.date() |
DateTime |
Date aleatoire |
faker.pastDate() |
DateTime |
Date dans le passe | |
faker.futureDate() |
DateTime |
Date dans le futur | |
| Texte | faker.lorem() |
String |
Mots lorem ipsum |
faker.sentences() |
String |
Plusieurs phrases | |
faker.paragraphs() |
String |
Plusieurs paragraphes | |
faker.slug() |
String |
Slug d'URL | |
| Web | faker.url() |
String |
Chaine d'URL |
faker.imageUrl() |
String |
URL d'image (via picsum.photos) | |
faker.ipAddress() |
String |
Adresse IPv4 | |
faker.macAddress() |
String |
Adresse MAC | |
| Localisation | faker.address() |
String |
Adresse postale |
faker.city() |
String |
Nom de ville | |
faker.state() |
String |
Abreviation d'etat americain | |
faker.zipCode() |
String |
Code postal | |
faker.country() |
String |
Nom de pays | |
| Autre | faker.hexColor() |
String |
Code couleur hexadecimal |
faker.creditCardNumber() |
String |
Numero de carte de credit | |
faker.randomElement(list) |
T |
Element aleatoire d'une liste | |
faker.randomElements(list, count) |
List<T> |
Elements aleatoires d'une liste |
Cache de test
NyTestCache fournit un cache en memoire pour tester les fonctionnalites liees au cache :
nyTest('cache operations', () async {
NyTestCache cache = NyTest.cache;
// Store a value
await cache.put<String>("key", "value");
// Store with expiration
await cache.put<String>("temp", "data", seconds: 60);
// Read a value
String? value = await cache.get<String>("key");
// Check existence
bool exists = await cache.has("key");
// Clear a key
await cache.clear("key");
// Flush all
await cache.flush();
// Get cache info
int size = await cache.size();
List<String> keys = await cache.documents();
});
Simulation de canaux de plateforme
NyMockChannels simule automatiquement les canaux de plateforme courants pour eviter les plantages dans les tests :
void main() {
NyTest.init(); // Automatically sets up mock channels
// Or set up manually
NyMockChannels.setup();
}
Canaux simules
- path_provider -- repertoires de documents, temporaires, support d'application, bibliotheque et cache
- flutter_secure_storage -- stockage securise en memoire
- flutter_timezone -- donnees de fuseau horaire
- flutter_local_notifications -- canal de notifications
- sqflite -- operations de base de donnees
Remplacer les chemins
NyMockChannels.overridePathProvider(
documentsPath: '/custom/documents',
temporaryPath: '/custom/temp',
);
Stockage securise dans les tests
NyMockChannels.setSecureStorageValue("token", "test_abc123");
Map<String, String> storage = NyMockChannels.getSecureStorage();
NyMockChannels.clearSecureStorage();
Simulation de Route Guard
NyMockRouteGuard vous permet de tester le comportement des route guards sans authentification reelle ni appels reseau. Il etend NyRouteGuard et fournit des constructeurs factory pour les scenarios courants.
Guard qui passe toujours
final guard = NyMockRouteGuard.pass();
Guard qui redirige
final guard = NyMockRouteGuard.redirect('/login');
// With additional data
final guard = NyMockRouteGuard.redirect('/error', data: {'code': 403});
Guard avec logique personnalisee
final guard = NyMockRouteGuard.custom((context) async {
if (context.data == null) {
return GuardResult.handled; // abort navigation
}
return GuardResult.next; // allow navigation
});
Suivi des appels de guard
Apres l'invocation d'un guard, vous pouvez inspecter son etat :
expect(guard.wasCalled, isTrue);
expect(guard.callCount, 1);
// Access the RouteContext from the last call
RouteContext? context = guard.lastContext;
// Reset tracking
guard.reset();
Assertions
Nylo Website fournit des fonctions d'assertion personnalisees :
Assertions de route
expectRoute('/home'); // Assert current route
expectNotRoute('/login'); // Assert not on route
expectRouteInHistory('/home'); // Assert route was visited
expectRouteExists('/profile'); // Assert route is registered
expectRoutesExist(['/home', '/profile', '/settings']);
Assertions d'etat
expectBackpackContains("key"); // Key exists
expectBackpackContains("key", value: "expected"); // Key has value
expectBackpackNotContains("key"); // Key doesn't exist
Assertions d'authentification
expectAuthenticated<User>(); // User is authenticated
expectGuest(); // No user authenticated
Assertions d'environnement
expectEnv("APP_NAME", "MyApp"); // Env variable equals value
expectEnvSet("APP_KEY"); // Env variable is set
Assertions de mode
expectTestMode();
expectDebugMode();
expectProductionMode();
expectDevelopingMode();
Assertions d'API
expectApiCalled('/users');
expectApiCalled('/users', method: 'POST', times: 2);
expectApiNotCalled('/admin');
Assertions de locale
expectLocale("en");
Assertions de toast
Verifiez les notifications toast enregistrees pendant un test. Necessite NyToastRecorder.setup() dans votre setUp de test :
setUp(() {
NyToastRecorder.setup();
});
nyWidgetTest('shows success toast', (tester) async {
await tester.pumpNyWidget(MyPage());
// ... trigger action that shows a toast ...
expectToastShown(id: 'success');
expectToastShown(id: 'danger', description: 'Something went wrong');
expectNoToastShown(id: 'info');
});
NyToastRecorder suit les notifications toast pendant les tests :
// Record a toast manually
NyToastRecorder.record(id: 'success', title: 'Done', description: 'Saved!');
// Check if a toast was shown
bool shown = NyToastRecorder.wasShown(id: 'success');
// Access all recorded toasts
List<ToastRecord> toasts = NyToastRecorder.records;
// Clear recorded toasts
NyToastRecorder.clear();
Assertions de verrouillage et de chargement
Verifiez les etats de verrouillage et de chargement nommes dans les widgets NyPage/NyState :
// Assert a named lock is held
expectLocked(tester, find.byType(MyPage), 'submit');
// Assert a named lock is not held
expectNotLocked(tester, find.byType(MyPage), 'submit');
// Assert a named loading key is active
expectLoadingNamed(tester, find.byType(MyPage), 'fetchUsers');
// Assert a named loading key is not active
expectNotLoadingNamed(tester, find.byType(MyPage), 'fetchUsers');
Matchers personnalises
Utilisez des matchers personnalises avec expect() :
// Type matcher
expect(result, isType<User>());
// Route name matcher
expect(widget, hasRouteName('/home'));
// Backpack matcher
expect(true, backpackHas("key", value: "expected"));
// API call matcher
expect(true, apiWasCalled('/users', method: 'GET', times: 1));
Test d'etat
Testez la gestion d'etat pilotee par EventBus dans les widgets NyPage et NyState en utilisant les aides de test d'etat.
Emettre des mises a jour d'etat
Simulez des mises a jour d'etat qui viendraient normalement d'un autre widget ou controlleur :
// Fire an UpdateState event
fireStateUpdate('HomePageState', data: {'items': ['a', 'b']});
await tester.pump();
expect(find.text('a'), findsOneWidget);
Emettre des actions d'etat
Envoyez des actions d'etat gerees par whenStateAction() dans votre page :
fireStateAction('HomePageState', 'refresh-page');
await tester.pump();
// With additional data
fireStateAction('CartState', 'add-item', data: {'id': 42});
await tester.pump();
Assertions d'etat
// Assert a state update was fired
expectStateUpdated('HomePageState');
expectStateUpdated('HomePageState', times: 2);
// Assert a state action was fired
expectStateAction('HomePageState', 'refresh-page');
expectStateAction('CartState', 'add-item', times: 1);
// Assert on the stateData of a NyPage/NyState widget
expectStateData(tester, find.byType(MyWidget), equals(42));
NyStateTestHelpers
Suivez et inspectez les mises a jour et actions d'etat emises :
// Get all updates fired to a state
List updates = NyStateTestHelpers.getUpdatesFor('MyWidget');
// Get all actions fired to a state
List actions = NyStateTestHelpers.getActionsFor('MyWidget');
// Reset all tracked state updates and actions
NyStateTestHelpers.reset();
Debogage
dump
Affiche l'etat actuel du test (contenu du Backpack, utilisateur authentifie, heure, appels API, locale) :
NyTest.dump();
dd (Dump and Die)
Affiche l'etat du test et termine immediatement le test :
NyTest.dd();
Stockage d'etat de test
Stockez et recuperez des valeurs pendant un test :
NyTest.set("step", "completed");
String? step = NyTest.get<String>("step");
Pre-remplir Backpack
Pre-remplissez Backpack avec des donnees de test :
NyTest.seedBackpack({
"user_name": "Anthony",
"auth_token": "test_token",
"settings": {"theme": "dark"},
});
Exemples
Fichier de test complet
import 'package:flutter_test/flutter_test.dart';
import 'package:nylo_framework/nylo_framework.dart';
void main() {
NyTest.init();
nyGroup('User Authentication', () {
nyTest('can authenticate a user', () async {
NyFactory.define<User>((faker) => User(
name: faker.name(),
email: faker.email(),
));
User user = NyFactory.make<User>();
NyTest.actingAs<User>(user);
expectAuthenticated<User>();
});
nyTest('guest has no access', () async {
expectGuest();
});
});
nyGroup('API Integration', () {
nyTest('can fetch users', () async {
NyMockApi.setRecordCalls(true);
NyMockApi.respond('/api/users', {
'users': [
{'id': 1, 'name': 'Anthony'},
{'id': 2, 'name': 'Jane'},
]
});
// ... trigger API call ...
expectApiCalled('/api/users');
});
});
nyGroup('Time-Sensitive Features', () {
nyTest('subscription expires correctly', () async {
NyTest.travel(DateTime(2025, 1, 1));
// Test subscription logic at a known date
expect(NyTime.now().year, equals(2025));
NyTest.travelForward(Duration(days: 365));
expect(NyTime.now().year, equals(2026));
NyTest.travelBack();
});
});
}