Advanced

Testen

Einleitung

Nylo Website v7 enthaelt ein umfassendes Test-Framework, inspiriert von Laravels Test-Utilities. Es bietet:

  • Testfunktionen mit automatischem Setup/Teardown (nyTest, nyWidgetTest, nyGroup)
  • Authentifizierungssimulation ueber NyTest.actingAs<T>()
  • Zeitreisen zum Einfrieren oder Manipulieren der Zeit in Tests
  • API-Mocking mit URL-Musterabgleich und Aufrufverfolgung
  • Factories mit einem integrierten Fake-Datengenerator (NyFaker)
  • Platform-Channel-Mocking fuer Secure Storage, Path Provider und mehr
  • Benutzerdefinierte Assertions fuer Routen, Backpack, Authentifizierung und Umgebung

Erste Schritte

Initialisieren Sie das Test-Framework am Anfang Ihrer Testdatei:

import 'package:nylo_framework/nylo_framework.dart';

void main() {
  NyTest.init();

  nyTest('my first test', () async {
    expect(1 + 1, equals(2));
  });
}

NyTest.init() richtet die Testumgebung ein und aktiviert das automatische Zuruecksetzen des Zustands zwischen Tests, wenn autoReset: true (Standardeinstellung).

Tests schreiben

nyTest

Die primaere Funktion zum Schreiben von Tests:

nyTest('can save and read from storage', () async {
  backpackSave("name", "Anthony");
  expect(backpackRead<String>("name"), equals("Anthony"));
});

Optionen:

nyTest('my test', () async {
  // test body
}, skip: false, timeout: Timeout(Duration(seconds: 30)));

nyWidgetTest

Zum Testen von Flutter-Widgets mit einem 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);
});

Widget-Test-Hilfsmittel

Die Klasse NyWidgetTest und die WidgetTester-Erweiterungen bieten Hilfsfunktionen zum Pumpen von Nylo-Widgets mit korrekter Theme-Unterstuetzung, zum Warten auf den Abschluss von init() und zum Testen von Ladezustaenden.

Testumgebung konfigurieren

Rufen Sie NyWidgetTest.configure() in Ihrem setUpAll auf, um das Laden von Google Fonts zu deaktivieren und optional ein benutzerdefiniertes Theme festzulegen:

nySetUpAll(() async {
  NyWidgetTest.configure(testTheme: ThemeData.light());
  await setupApplication(providers);
});

Sie koennen die Konfiguration mit NyWidgetTest.reset() zuruecksetzen.

Zwei integrierte Themes sind fuer schriftfreies Testen verfuegbar:

ThemeData light = NyWidgetTest.simpleTestTheme;
ThemeData dark = NyWidgetTest.simpleDarkTestTheme;

Nylo-Widgets pumpen

Verwenden Sie pumpNyWidget, um ein Widget in eine MaterialApp mit Theme-Unterstuetzung einzubetten:

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

Fuer schnelles Pumpen mit einem schriftfreien Theme:

await tester.pumpNyWidgetSimple(HomePage());

Auf Initialisierung warten

pumpNyWidgetAndWaitForInit pumpt Frames, bis die Ladeanzeigen verschwinden (oder das Timeout erreicht ist), was nützlich für Seiten mit asynchronen init()-Methoden ist:

await tester.pumpNyWidgetAndWaitForInit(
  HomePage(),
  timeout: Duration(seconds: 10),
  useSimpleTheme: true,
);
// init() has completed
expect(find.text('Loaded Data'), findsOneWidget);

Pump-Hilfsfunktionen

// 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));

Lebenszyklus-Simulation

Simulieren Sie AppLifecycleState-Aenderungen bei jedem NyPage im Widget-Baum:

await tester.pumpNyWidget(MyPage());
await tester.simulateLifecycleState(AppLifecycleState.paused);
await tester.pump();
// Assert side effects of the paused lifecycle action

Lade- und Sperr-Ueberpruefungen

Ueberpruefen Sie benannte Ladeschluessel und Sperren auf NyPage/NyState-Widgets:

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

testNyPage-Hilfsfunktion

Eine praktische Funktion, die ein NyPage pumpt, auf die Initialisierung wartet und dann Ihre Erwartungen ausfuehrt:

testNyPage(
  'HomePage loads correctly',
  build: () => HomePage(),
  expectations: (tester) async {
    expect(find.text('Welcome'), findsOneWidget);
  },
  useSimpleTheme: true,
  initTimeout: Duration(seconds: 10),
  skip: false,
);

testNyPageLoading-Hilfsfunktion

Testen Sie, dass eine Seite waehrend init() einen Ladeindikator anzeigt:

testNyPageLoading(
  'HomePage shows loading state',
  build: () => HomePage(),
  skip: false,
);

NyPageTestMixin

Ein Mixin mit gaengigen Seitentest-Hilfsmitteln:

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

Zusammengehoerige Tests gruppieren:

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

Test-Lebenszyklus

Setup- und Teardown-Logik mit Lebenszyklus-Hooks einrichten:

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

Tests ueberspringen und CI-Tests

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

Authentifizierung

Authentifizierte Benutzer in Tests simulieren:

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

Den Benutzer abmelden:

NyTest.logout();
expectGuest();

Zeitreisen

Manipulieren Sie die Zeit in Ihren Tests mit NyTime:

Zu einem bestimmten Datum springen

nyTest('time travel to 2025', () async {
  NyTest.travel(DateTime(2025, 1, 1));

  expect(NyTime.now().year, equals(2025));

  NyTest.travelBack(); // Reset to real time
});

Zeit vor- oder zurueckspulen

NyTest.travelForward(Duration(days: 30)); // Jump 30 days ahead
NyTest.travelBackward(Duration(hours: 2)); // Go back 2 hours

Zeit einfrieren

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

Zeitgrenzen

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

Zeitreisen mit begrenztem Gueltigkeitsbereich

Code innerhalb eines eingefrorenen Zeitkontexts ausfuehren:

await NyTime.withFrozenTime<void>(DateTime(2025, 6, 15), () async {
  expect(NyTime.now(), equals(DateTime(2025, 6, 15)));
});
// Time is automatically restored after the callback

API-Mocking

Mocking nach URL-Muster

API-Antworten mit URL-Mustern und Wildcard-Unterstuetzung mocken:

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

Mocking nach API-Service-Typ

Einen gesamten API-Service nach Typ mocken:

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

Aufrufverlauf und Assertions

API-Aufrufe verfolgen und ueberpruefen:

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

Mock-Antworten erstellen

Response<Map<String, dynamic>> response = NyMockApi.createResponse(
  data: {'id': 1, 'name': 'Anthony'},
  statusCode: 200,
  statusMessage: 'OK',
);

Factories

Factories definieren

Definieren Sie, wie Testinstanzen Ihrer Modelle erstellt werden:

NyFactory.define<User>((NyFaker faker) => User(
  name: faker.name(),
  email: faker.email(),
  phone: faker.phone(),
));

Mit Unterstuetzung fuer Ueberschreibungen:

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

Factory-Zustaende

Variationen einer Factory definieren:

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

Instanzen erstellen

// 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 generiert realistische Testdaten. Es ist innerhalb von Factory-Definitionen verfuegbar und kann auch direkt instanziiert werden.

NyFaker faker = NyFaker();

Verfuegbare Methoden

Kategorie Methode Rueckgabetyp Beschreibung
Namen faker.firstName() String Zufaelliger Vorname
faker.lastName() String Zufaelliger Nachname
faker.name() String Vollstaendiger Name (Vor- + Nachname)
faker.username() String Benutzername
Kontakt faker.email() String E-Mail-Adresse
faker.phone() String Telefonnummer
faker.company() String Firmenname
Zahlen faker.randomInt(min, max) int Zufaellige Ganzzahl im Bereich
faker.randomDouble(min, max) double Zufaellige Gleitkommazahl im Bereich
faker.randomBool() bool Zufaelliger Wahrheitswert
Bezeichner faker.uuid() String UUID-v4-String
Datum faker.date() DateTime Zufaelliges Datum
faker.pastDate() DateTime Datum in der Vergangenheit
faker.futureDate() DateTime Datum in der Zukunft
Text faker.lorem() String Lorem-ipsum-Woerter
faker.sentences() String Mehrere Saetze
faker.paragraphs() String Mehrere Absaetze
faker.slug() String URL-Slug
Web faker.url() String URL-String
faker.imageUrl() String Bild-URL (via picsum.photos)
faker.ipAddress() String IPv4-Adresse
faker.macAddress() String MAC-Adresse
Standort faker.address() String Strassenadresse
faker.city() String Stadtname
faker.state() String US-Bundesstaatabkuerzung
faker.zipCode() String Postleitzahl
faker.country() String Laendername
Sonstiges faker.hexColor() String Hex-Farbcode
faker.creditCardNumber() String Kreditkartennummer
faker.randomElement(list) T Zufaelliges Element aus einer Liste
faker.randomElements(list, count) List<T> Zufaellige Elemente aus einer Liste

Test-Cache

NyTestCache bietet einen In-Memory-Cache zum Testen von Cache-bezogener Funktionalitaet:

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

Platform-Channel-Mocking

NyMockChannels mockt automatisch gaengige Platform-Channels, damit Tests nicht abstuerzen:

void main() {
  NyTest.init(); // Automatically sets up mock channels

  // Or set up manually
  NyMockChannels.setup();
}

Gemockte Channels

  • path_provider -- Dokumente, temporaere Dateien, Application-Support, Bibliothek und Cache-Verzeichnisse
  • flutter_secure_storage -- In-Memory-Secure-Storage
  • flutter_timezone -- Zeitzonendaten
  • flutter_local_notifications -- Benachrichtigungskanal
  • sqflite -- Datenbankoperationen

Pfade ueberschreiben

NyMockChannels.overridePathProvider(
  documentsPath: '/custom/documents',
  temporaryPath: '/custom/temp',
);

Secure Storage in Tests

NyMockChannels.setSecureStorageValue("token", "test_abc123");

Map<String, String> storage = NyMockChannels.getSecureStorage();
NyMockChannels.clearSecureStorage();

Route-Guard-Mocking

NyMockRouteGuard ermoeglicht es Ihnen, das Verhalten von Route Guards ohne echte Authentifizierung oder Netzwerkaufrufe zu testen. Es erweitert NyRouteGuard und bietet Factory-Konstruktoren fuer gaengige Szenarien.

Guard, der immer durchlaesst

final guard = NyMockRouteGuard.pass();

Guard, der umleitet

final guard = NyMockRouteGuard.redirect('/login');

// With additional data
final guard = NyMockRouteGuard.redirect('/error', data: {'code': 403});

Guard mit benutzerdefinierter Logik

final guard = NyMockRouteGuard.custom((context) async {
  if (context.data == null) {
    return GuardResult.handled; // abort navigation
  }
  return GuardResult.next; // allow navigation
});

Guard-Aufrufe verfolgen

Nachdem ein Guard aufgerufen wurde, koennen Sie seinen Zustand ueberpruefen:

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 stellt benutzerdefinierte Assertion-Funktionen bereit:

Routen-Assertions

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']);

Zustands-Assertions

expectBackpackContains("key");                        // Key exists
expectBackpackContains("key", value: "expected");     // Key has value
expectBackpackNotContains("key");                     // Key doesn't exist

Auth-Assertions

expectAuthenticated<User>();  // User is authenticated
expectGuest();                // No user authenticated

Umgebungs-Assertions

expectEnv("APP_NAME", "MyApp");  // Env variable equals value
expectEnvSet("APP_KEY");          // Env variable is set

Modus-Assertions

expectTestMode();
expectDebugMode();
expectProductionMode();
expectDevelopingMode();

API-Assertions

expectApiCalled('/users');
expectApiCalled('/users', method: 'POST', times: 2);
expectApiNotCalled('/admin');

Locale-Assertions

expectLocale("en");

Toast-Assertions

Ueberpruefen Sie Toast-Benachrichtigungen, die waehrend eines Tests aufgezeichnet wurden. Erfordert NyToastRecorder.setup() in Ihrem Test-setUp:

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 verfolgt Toast-Benachrichtigungen waehrend der 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();

Sperr- und Lade-Assertions

Ueberpruefen Sie benannte Sperr- und Ladezustaende in NyPage/NyState-Widgets:

// 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');

Benutzerdefinierte Matcher

Benutzerdefinierte Matcher mit expect() verwenden:

// 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));

State-Testing

Testen Sie EventBus-gesteuerte Zustandsverwaltung in NyPage- und NyState-Widgets mit State-Test-Hilfsfunktionen.

State-Updates ausloesen

Simulieren Sie State-Updates, die normalerweise von einem anderen Widget oder Controller kommen wuerden:

// Fire an UpdateState event
fireStateUpdate('HomePageState', data: {'items': ['a', 'b']});
await tester.pump();
expect(find.text('a'), findsOneWidget);

State-Actions ausloesen

Senden Sie State-Actions, die von whenStateAction() in Ihrer Seite verarbeitet werden:

fireStateAction('HomePageState', 'refresh-page');
await tester.pump();

// With additional data
fireStateAction('CartState', 'add-item', data: {'id': 42});
await tester.pump();

State-Assertions

// 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

Verfolgen und ueberpruefen Sie ausgeloeste State-Updates und Actions:

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

Debugging

dump

Den aktuellen Testzustand ausgeben (Backpack-Inhalte, Auth-Benutzer, Zeit, API-Aufrufe, Locale):

NyTest.dump();

dd (Dump and Die)

Den Testzustand ausgeben und den Test sofort beenden:

NyTest.dd();

Testzustandsspeicher

Werte waehrend eines Tests speichern und abrufen:

NyTest.set("step", "completed");
String? step = NyTest.get<String>("step");

Backpack mit Testdaten befuellen

Backpack mit Testdaten vorab befuellen:

NyTest.seedBackpack({
  "user_name": "Anthony",
  "auth_token": "test_token",
  "settings": {"theme": "dark"},
});

Beispiele

Vollstaendige Testdatei

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