Testing
Introduction
Nylo Website v7 includes a comprehensive testing framework inspired by Laravel's testing utilities. It provides:
- Test functions with automatic setup/teardown (
nyTest,nyWidgetTest,nyGroup) - Authentication simulation via
NyTest.actingAs<T>() - Time travel to freeze or manipulate time in tests
- API mocking with URL pattern matching and call tracking
- Factories with a built-in fake data generator (
NyFaker) - Platform channel mocking for secure storage, path provider, and more
- Custom assertions for routes, Backpack, authentication, and environment
Getting Started
Initialize the test framework at the top of your test file:
import 'package:nylo_framework/nylo_framework.dart';
void main() {
NyTest.init();
nyTest('my first test', () async {
expect(1 + 1, equals(2));
});
}
NyTest.init() sets up the test environment and enables automatic state reset between tests when autoReset: true (the default).
Writing Tests
nyTest
The primary function for writing 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
For testing Flutter widgets with a 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 Testing Utilities
The NyWidgetTest class and WidgetTester extensions provide helpers for pumping Nylo widgets with proper theme support, waiting for init() to complete, and testing loading states.
Configuring the Test Environment
Call NyWidgetTest.configure() in your setUpAll to disable Google Fonts fetching and optionally set a custom theme:
nySetUpAll(() async {
NyWidgetTest.configure(testTheme: ThemeData.light());
await setupApplication(providers);
});
You can reset the configuration with NyWidgetTest.reset().
Two built-in themes are available for font-free testing:
ThemeData light = NyWidgetTest.simpleTestTheme;
ThemeData dark = NyWidgetTest.simpleDarkTestTheme;
Pumping Nylo Widgets
Use pumpNyWidget to wrap a widget in a MaterialApp with theme support:
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);
});
For a quick pump with a font-free theme:
await tester.pumpNyWidgetSimple(HomePage());
Waiting for Init
pumpNyWidgetAndWaitForInit pumps frames until loading indicators disappear (or the timeout is reached), which is useful for pages with async init() methods:
await tester.pumpNyWidgetAndWaitForInit(
HomePage(),
timeout: Duration(seconds: 10),
useSimpleTheme: true,
);
// init() has completed
expect(find.text('Loaded Data'), findsOneWidget);
Pump Helpers
// 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));
Lifecycle Simulation
Simulate AppLifecycleState changes on any NyPage in the widget tree:
await tester.pumpNyWidget(MyPage());
await tester.simulateLifecycleState(AppLifecycleState.paused);
await tester.pump();
// Assert side effects of the paused lifecycle action
Loading and Lock Checks
Check named loading keys and locks on 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 Helper
A convenience function that pumps a NyPage, waits for init, then runs your expectations:
testNyPage(
'HomePage loads correctly',
build: () => HomePage(),
expectations: (tester) async {
expect(find.text('Welcome'), findsOneWidget);
},
useSimpleTheme: true,
initTimeout: Duration(seconds: 10),
skip: false,
);
testNyPageLoading Helper
Test that a page displays a loading indicator during init():
testNyPageLoading(
'HomePage shows loading state',
build: () => HomePage(),
skip: false,
);
NyPageTestMixin
A mixin providing common page testing utilities:
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
Group related tests together:
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 Lifecycle
Set up and tear down logic using lifecycle hooks:
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
});
}
Skipping and 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
});
Authentication
Simulate authenticated users in 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();
});
Log the user out:
NyTest.logout();
expectGuest();
Time Travel
Manipulate time in your tests using NyTime:
Jump to a Specific Date
nyTest('time travel to 2025', () async {
NyTest.travel(DateTime(2025, 1, 1));
expect(NyTime.now().year, equals(2025));
NyTest.travelBack(); // Reset to real time
});
Advance or Rewind Time
NyTest.travelForward(Duration(days: 30)); // Jump 30 days ahead
NyTest.travelBackward(Duration(hours: 2)); // Go back 2 hours
Freeze Time
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
Time Boundaries
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
Scoped Time Travel
Execute code within a frozen time context:
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 by URL Pattern
Mock API responses using URL patterns with wildcard support:
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 by API Service Type
Mock an entire API service by 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'};
});
});
Call History and Assertions
Track and verify API calls:
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');
});
Creating Mock Responses
Response<Map<String, dynamic>> response = NyMockApi.createResponse(
data: {'id': 1, 'name': 'Anthony'},
statusCode: 200,
statusMessage: 'OK',
);
Factories
Defining Factories
Define how to create test instances of your models:
NyFactory.define<User>((NyFaker faker) => User(
name: faker.name(),
email: faker.email(),
phone: faker.phone(),
));
With override support:
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 States
Define variations of a 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');
});
Creating 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 generates realistic fake data for tests. It's available inside factory definitions and can be instantiated directly.
NyFaker faker = NyFaker();
Available Methods
| Category | Method | Return Type | Description |
|---|---|---|---|
| Names | faker.firstName() |
String |
Random first name |
faker.lastName() |
String |
Random last name | |
faker.name() |
String |
Full name (first + last) | |
faker.username() |
String |
Username string | |
| Contact | faker.email() |
String |
Email address |
faker.phone() |
String |
Phone number | |
faker.company() |
String |
Company name | |
| Numbers | faker.randomInt(min, max) |
int |
Random integer in range |
faker.randomDouble(min, max) |
double |
Random double in range | |
faker.randomBool() |
bool |
Random boolean | |
| Identifiers | faker.uuid() |
String |
UUID v4 string |
| Dates | faker.date() |
DateTime |
Random date |
faker.pastDate() |
DateTime |
Date in the past | |
faker.futureDate() |
DateTime |
Date in the future | |
| Text | faker.lorem() |
String |
Lorem ipsum words |
faker.sentences() |
String |
Multiple sentences | |
faker.paragraphs() |
String |
Multiple paragraphs | |
faker.slug() |
String |
URL slug | |
| Web | faker.url() |
String |
URL string |
faker.imageUrl() |
String |
Image URL (via picsum.photos) | |
faker.ipAddress() |
String |
IPv4 address | |
faker.macAddress() |
String |
MAC address | |
| Location | faker.address() |
String |
Street address |
faker.city() |
String |
City name | |
faker.state() |
String |
US state abbreviation | |
faker.zipCode() |
String |
Zip code | |
faker.country() |
String |
Country name | |
| Other | faker.hexColor() |
String |
Hex color code |
faker.creditCardNumber() |
String |
Credit card number | |
faker.randomElement(list) |
T |
Random item from list | |
faker.randomElements(list, count) |
List<T> |
Random items from list |
Test Cache
NyTestCache provides an in-memory cache for testing cache-related functionality:
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 automatically mocks common platform channels so tests don't crash:
void main() {
NyTest.init(); // Automatically sets up mock channels
// Or set up manually
NyMockChannels.setup();
}
Mocked Channels
- path_provider -- documents, temporary, application support, library, and cache directories
- flutter_secure_storage -- in-memory secure storage
- flutter_timezone -- timezone data
- flutter_local_notifications -- notification channel
- sqflite -- database operations
Override Paths
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 lets you test route guard behavior without real authentication or network calls. It extends NyRouteGuard and provides factory constructors for common scenarios.
Guard That Always Passes
final guard = NyMockRouteGuard.pass();
Guard That Redirects
final guard = NyMockRouteGuard.redirect('/login');
// With additional data
final guard = NyMockRouteGuard.redirect('/error', data: {'code': 403});
Guard With Custom Logic
final guard = NyMockRouteGuard.custom((context) async {
if (context.data == null) {
return GuardResult.handled; // abort navigation
}
return GuardResult.next; // allow navigation
});
Tracking Guard Calls
After a guard has been invoked you can inspect its state:
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 provides custom assertion functions:
Route 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']);
State 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
Environment Assertions
expectEnv("APP_NAME", "MyApp"); // Env variable equals value
expectEnvSet("APP_KEY"); // Env variable is set
Mode Assertions
expectTestMode();
expectDebugMode();
expectProductionMode();
expectDevelopingMode();
API Assertions
expectApiCalled('/users');
expectApiCalled('/users', method: 'POST', times: 2);
expectApiNotCalled('/admin');
Locale Assertions
expectLocale("en");
Toast Assertions
Assert on toast notifications that were recorded during a test. Requires NyToastRecorder.setup() in your 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 tracks toast notifications during 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();
Lock and Loading Assertions
Assert on named lock and loading states 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');
Custom Matchers
Use custom matchers with 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));
State Testing
Test EventBus-driven state management in NyPage and NyState widgets using state test helpers.
Firing State Updates
Simulate state updates that would normally come from another widget or controller:
// Fire an UpdateState event
fireStateUpdate('HomePageState', data: {'items': ['a', 'b']});
await tester.pump();
expect(find.text('a'), findsOneWidget);
Firing State Actions
Send state actions that are handled by whenStateAction() in your page:
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
Track and inspect fired state updates and 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
Print the current test state (Backpack contents, auth user, time, API calls, locale):
NyTest.dump();
dd (Dump and Die)
Print the test state and immediately terminate the test:
NyTest.dd();
Test State Storage
Store and retrieve values during a test:
NyTest.set("step", "completed");
String? step = NyTest.get<String>("step");
Seed Backpack
Pre-populate Backpack with test data:
NyTest.seedBackpack({
"user_name": "Anthony",
"auth_token": "test_token",
"settings": {"theme": "dark"},
});
Examples
Full Test File
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();
});
});
}