Advanced

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();
    });
  });
}