Skip to content
· 8 min read · 0 views

Flutter Fundamental: Testing & Scheduler

Learn how to test Flutter apps with unit, widget, and integration tests, plus scheduling background tasks with timers, WorkManager, and alarm managers.

// table of contents (30 sections)

Why Testing Matters

You’ve built your Flutter app. It works on your device. But what happens when:

  • A user enters unexpected input?
  • The API returns an error?
  • A new feature breaks existing behavior?

Without tests, you guess. With tests, you know.

“Untested code is legacy code from day one.” — Unknown

Flutter provides three levels of testing, each serving a different purpose.


The Testing Pyramid

        ╱ ╲
       ╱ E2E╲          ← Few, slow, expensive
      ╱───────╲
     ╱ Widget  ╲       ← Medium amount
    ╱───────────╲
   ╱  Unit Tests ╲     ← Many, fast, cheap
  ╱───────────────╲
Test TypeWhat It TestsSpeedCount
UnitFunctions, classes, logicMillisecondsMany
WidgetSingle widget behaviorSecondsMedium
IntegrationFull app flowsSeconds/MinutesFew

Unit Testing

Unit tests verify individual functions and classes in isolation. They’re the fastest and should make up the majority of your tests.

Setup

Add to pubspec.yaml:

dev_dependencies:
  flutter_test:
    sdk: flutter

Basic Unit Test

// lib/calculator.dart
class Calculator {
  double add(double a, double b) => a + b;
  double subtract(double a, double b) => a - b;
  double multiply(double a, double b) => a * b;
  double divide(double a, double b) {
    if (b == 0) throw ArgumentError('Cannot divide by zero');
    return a / b;
  }
}
// test/calculator_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/calculator.dart';

void main() {
  late Calculator calculator;

  setUp(() {
    calculator = Calculator();
  });

  group('Calculator', () {
    test('add returns sum of two numbers', () {
      expect(calculator.add(2, 3), equals(5));
    });

    test('subtract returns difference', () {
      expect(calculator.subtract(10, 4), equals(6));
    });

    test('multiply returns product', () {
      expect(calculator.multiply(3, 4), equals(12));
    });

    test('divide throws on zero divisor', () {
      expect(
        () => calculator.divide(10, 0),
        throwsArgumentError,
      );
    });
  });
}

Matchers

Flutter provides rich matchers for assertions:

// Equality
expect(value, equals(42));
expect(name, equals('Abdu'));

// Boolean
expect(isValid, isTrue);
expect(isEmpty, isFalse);

// Collections
expect(list, contains('dart'));
expect(list, isEmpty);
expect(list, hasLength(3));

// Type
expect(result, isA<String>());
expect(error, isA<FormatException>());

// Numeric
expect(pi, closeTo(3.14, 0.01));
expect(age, greaterThan(18));

// Exception
expect(() => riskyOperation(), throwsException);

Testing Async Code

test('fetchUsers returns list of users', () async {
  final users = await fetchUsers();
  expect(users, isNotEmpty);
  expect(users.first, isA<User>());
});

test('stream emits values in order', () {
  expect(
    countDown(),
    emitsInOrder([5, 4, 3, 2, 1]),
  );
});

Mocking with Mockito

When your class depends on external services, use mocks:

import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';

// Generate mocks with: flutter pub run build_runner build
@GenerateMocks([ApiService])
import 'user_test.mocks.dart';

void main() {
  late MockApiService mockApi;
  late UserRepository repository;

  setUp(() {
    mockApi = MockApiService();
    repository = UserRepository(apiService: mockApi);
  });

  test('getUsers returns users from API', () async {
    // Arrange
    when(mockApi.fetchUsers()).thenAnswer((_) async => [
      {'id': 1, 'name': 'Abdu'},
    ]);

    // Act
    final users = await repository.getUsers();

    // Assert
    expect(users.length, equals(1));
    expect(users.first.name, equals('Abdu'));
    verify(mockApi.fetchUsers()).called(1);
  });

  test('getUsers throws on API failure', () async {
    when(mockApi.fetchUsers()).thenThrow(Exception('Network error'));

    expect(() => repository.getUsers(), throwsException);
  });
}

Widget Testing

Widget tests verify that individual widgets render correctly and respond to user interaction.

Basic Widget Test

testWidgets('Counter displays initial value', (tester) async {
  await tester.pumpWidget(
    MaterialApp(home: Counter()),
  );

  expect(find.text('Count: 0'), findsOneWidget);
  expect(find.text('Count: 1'), findsNothing);
});

testWidgets('tapping increment button updates display', (tester) async {
  await tester.pumpWidget(
    MaterialApp(home: Counter()),
  );

  // Find and tap the button
  await tester.tap(find.text('Increment'));
  await tester.pump(); // Rebuild after state change

  expect(find.text('Count: 1'), findsOneWidget);
});

Finding Widgets

// By text
find.text('Hello')

// By type
find.byType(ElevatedButton)
find.byType(AppBar)

// By key
find.byKey(Key('submit-button'))

// By icon
find.byIcon(Icons.search)

// By widget
find.byWidget(myWidget)

// Finding ancestors/descendants
find.descendant(
  of: find.byType(ListView),
  matching: find.byType(ListTile),
)

User Interactions

// Tap
await tester.tap(find.text('Submit'));
await tester.pumpAndSettle();

// Enter text
await tester.enterText(find.byType(TextField), 'Hello Flutter');
await tester.pumpAndSettle();

// Long press
await tester.longPress(find.byType(Card));
await tester.pumpAndSettle();

// Scroll
await tester.scrollUntilVisible(
  find.text('Load More'),
  100.0,
  scrollable: find.byType(Scrollable),
);

// Drag
await tester.drag(find.byType(Dismissible), Offset(-300, 0));
await tester.pumpAndSettle();

Testing with Provider

When widgets depend on state management:

testWidgets('UserList shows users from provider', (tester) async {
  final mockNotifier = UserNotifier();
  mockNotifier.addUser(User(name: 'Test User'));

  await tester.pumpWidget(
    ChangeNotifierProvider<UserNotifier>.value(
      value: mockNotifier,
      child: MaterialApp(home: UserListPage()),
    ),
  );

  expect(find.text('Test User'), findsOneWidget);
});

Integration Testing

Integration tests run the full app on a real device or emulator. They verify complete user flows.

Setup

dev_dependencies:
  integration_test:
    sdk: flutter

Create integration_test/app_test.dart:

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('Login Flow', () {
    testWidgets('user can log in with valid credentials', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      // Enter email
      await tester.enterText(
        find.byKey(Key('email-field')),
        'test@example.com',
      );

      // Enter password
      await tester.enterText(
        find.byKey(Key('password-field')),
        'password123',
      );

      // Tap login
      await tester.tap(find.byKey(Key('login-button')));
      await tester.pumpAndSettle();

      // Verify we're on home page
      expect(find.text('Welcome'), findsOneWidget);
    });
  });
}

Running Integration Tests

# On connected device/emulator
flutter test integration_test/app_test.dart

# On specific platform
flutter test integration_test/app_test.dart -d chrome

Test Coverage

To measure how much of your code is tested:

flutter test --coverage

This generates coverage/lcov.info. Use lcov to view it:

# Install lcov (macOS)
brew install lcov

# Generate HTML report
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html

Coverage Targets

LevelTarget CoverageFocus Areas
Critical business logic90%+Calculations, payments, auth
ViewModels/BLoCs80%+State transitions, error handling
Widgets50-70%Key interactions, edge cases
IntegrationKey flowsLogin, checkout, core paths

Scheduling & Background Tasks

Not everything happens while the user is looking at the screen. Flutter apps often need to:

  • Run code after a delay
  • Periodically sync data
  • Download content in the background
  • Show scheduled notifications

Timer

For simple delayed or periodic tasks:

import 'dart:async';

// One-shot timer
Timer(Duration(seconds: 3), () {
  print('3 seconds passed!');
});

// Periodic timer
Timer.periodic(Duration(minutes: 5), (timer) {
  syncData();
  if (shouldStop) timer.cancel();
});

Future.delayed

For one-time delayed operations within async code:

Future<void> showWelcomeAfterDelay() async {
  await Future.delayed(Duration(seconds: 2));
  showWelcomeDialog();
}

WorkManager (android_alarm_manager_plus)

For background tasks that survive app restarts:

import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';

void callbackDispatcher() {
  // This runs in background isolate
  syncData();
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await AndroidAlarmManager.initialize();

  // Schedule periodic task (every 15 minutes)
  await AndroidAlarmManager.periodic(
    Duration(minutes: 15),
    0, // Unique ID
    callbackDispatcher,
    exact: false,
    wakeup: true,
  );

  runApp(MyApp());
}

workmanager Package

Cross-platform background task execution:

import 'package:workmanager/workmanager.dart';

@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) async {
    switch (task) {
      case 'syncData':
        await syncData();
        break;
      case 'downloadContent':
        await downloadContent();
        break;
    }
    return Future.value(true);
  });
}

void main() {
  Workmanager().initialize(callbackDispatcher, isInDebugMode: true);

  Workmanager().registerPeriodicTask(
    'sync-task',
    'syncData',
    frequency: Duration(hours: 1),
    constraints: Constraints(
      networkType: NetworkType.connected,
    ),
  );

  runApp(MyApp());
}

Scheduling Comparison

MethodPlatformSurvives RestartMinimum Interval
TimerAllNo1ms
Future.delayedAllNo1ms
android_alarm_managerAndroid onlyYes1 minute
workmanagerAndroid + iOSYes15 minutes
flutter_local_notificationsAllYesExact time

Scheduled Notifications

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

final FlutterLocalNotificationsPlugin notifications =
    FlutterLocalNotificationsPlugin();

// Initialize
await notifications.initialize(
  InitializationSettings(
    android: AndroidInitializationSettings('@mipmap/ic_launcher'),
    iOS: DarwinInitializationSettings(),
  ),
);

// Schedule notification
await notifications.schedule(
  0,
  'Daily Reminder',
  'Time to practice Flutter!',
  DateTime.now().add(Duration(hours: 8)),
  NotificationDetails(
    android: AndroidNotificationDetails(
      'daily-channel',
      'Daily Reminders',
      importance: Importance.high,
    ),
  ),
);

Best Practices

Testing

  1. Write tests first for critical business logic (TDD)
  2. Use descriptive test names that read like documentation
  3. One assertion per test when possible (focused tests)
  4. Mock external dependencies (APIs, databases, sensors)
  5. Run tests on every commit via CI/CD

Scheduling

  1. Minimize background work to save battery
  2. Use constraints (WiFi only, charging, etc.)
  3. Handle failures gracefully with retry logic
  4. Test background tasks in isolation first
  5. Inform users about background activity

Quick Reference

Test TypeFile LocationCommand
Unittest/flutter test
Widgettest/flutter test
Integrationintegration_test/flutter test integration_test/
Coveragecoverage/flutter test --coverage
SchedulerUse When
TimerShort delays, in-app periodic tasks
workmanagerBackground sync, periodic downloads
flutter_local_notificationsScheduled user reminders

What’s Next?

Testing and scheduling are the final pieces that make your Flutter app production-ready. Combine everything you’ve learned from Dart fundamentals, Flutter widgets, state management, networking, and now testing to build professional-grade mobile applications.

Bismillah, may your tests always pass! 🚀

You might also like

Enjoyed This Post?

Want to discuss the topic, have questions, or looking to collaborate on something similar? Drop a comment below or reach out directly.

Discussion