Question #47EasyFlutter Basics

Flutter Types of testing

#flutter#testing

Answer

Overview

Flutter supports three main types of testing to ensure app quality: Unit Testing, Widget Testing, and Integration Testing. Each type tests different aspects of your application.


1. Unit Testing

Purpose: Test individual functions, methods, or classes in isolation.

What to test:

  • Business logic
  • Data models
  • Utility functions
  • Repositories
  • Services

Example

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

void main() {
  group('Calculator', () {
    late Calculator calculator;

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

    test('add returns sum of two numbers', () {
      expect(calculator.add(2, 3), 5);
      expect(calculator.add(-1, 1), 0);
    });

    test('subtract returns difference', () {
      expect(calculator.subtract(5, 3), 2);
    });

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

    test('divide returns quotient', () {
      expect(calculator.divide(10, 2), 5.0);
    });

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

Running Unit Tests

bash
flutter test test/calculator_test.dart

2. Widget Testing

Purpose: Test UI components (widgets) and their interactions.

What to test:

  • Widget rendering
  • User interactions (tap, swipe, input)
  • Widget state changes
  • Widget tree structure

Example

dart
// lib/widgets/counter_widget.dart
class CounterWidget extends StatefulWidget {
  
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;

  void _increment() => setState(() => _counter++);
  void _decrement() => setState(() => _counter--);

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('$_counter', key: Key('counter_text')),
        ElevatedButton(
          key: Key('increment_button'),
          onPressed: _increment,
          child: Text('Increment'),
        ),
        ElevatedButton(
          key: Key('decrement_button'),
          onPressed: _decrement,
          child: Text('Decrement'),
        ),
      ],
    );
  }
}
dart
// test/widget/counter_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/counter_widget.dart';

void main() {
  group('CounterWidget', () {
    testWidgets('displays initial counter value of 0', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(home: CounterWidget()));

      expect(find.text('0'), findsOneWidget);
    });

    testWidgets('increments counter when increment button is tapped', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(home: CounterWidget()));

      // Tap increment button
      await tester.tap(find.byKey(Key('increment_button')));
      await tester.pump();

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

    testWidgets('decrements counter when decrement button is tapped', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(home: CounterWidget()));

      // Increment first
      await tester.tap(find.byKey(Key('increment_button')));
      await tester.pump();

      // Then decrement
      await tester.tap(find.byKey(Key('decrement_button')));
      await tester.pump();

      expect(find.text('0'), findsOneWidget);
    });

    testWidgets('finds widgets by type', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(home: CounterWidget()));

      expect(find.byType(ElevatedButton), findsNWidgets(2));
      expect(find.byType(Text), findsNWidgets(3)); // counter + 2 button labels
    });
  });
}

Common Widget Test Methods

dart
// Finding widgets
find.text('Hello');
find.byKey(Key('my_key'));
find.byType(ElevatedButton);
find.byIcon(Icons.add);
find.byWidget(myWidget);

// Interactions
await tester.tap(find.byKey(Key('button')));
await tester.enterText(find.byType(TextField), 'Hello');
await tester.drag(find.byType(ListView), Offset(0, -200));

// Pump methods
await tester.pump();              // Trigger a frame
await tester.pumpAndSettle();     // Wait for all animations to complete
await tester.pumpWidget(widget);  // Render widget

// Assertions
expect(find.text('Hello'), findsOneWidget);
expect(find.byType(Text), findsNWidgets(3));
expect(find.text('Missing'), findsNothing);

Running Widget Tests

bash
flutter test test/widget/counter_widget_test.dart

3. Integration Testing

Purpose: Test complete app or large parts working together (end-to-end testing).

What to test:

  • User flows (login → dashboard → logout)
  • Navigation between screens
  • Real device/emulator behavior
  • Performance
  • Interactions with backend APIs

Setup

yaml
# pubspec.yaml
dev_dependencies:
  integration_test:
    sdk: flutter
  flutter_test:
    sdk: flutter

Example

dart
// integration_test/app_test.dart
import 'package:flutter/material.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('App Integration Test', () {
    testWidgets('complete user flow', (WidgetTester tester) async {
      // Start app
      app.main();
      await tester.pumpAndSettle();

      // 1. Verify home page
      expect(find.text('Welcome'), findsOneWidget);

      // 2. Navigate to login
      await tester.tap(find.byKey(Key('login_button')));
      await tester.pumpAndSettle();

      // 3. Enter credentials
      await tester.enterText(find.byKey(Key('email_field')), 'test@example.com');
      await tester.enterText(find.byKey(Key('password_field')), 'password123');

      // 4. Submit login
      await tester.tap(find.byKey(Key('submit_button')));
      await tester.pumpAndSettle(Duration(seconds: 3)); // Wait for API

      // 5. Verify dashboard
      expect(find.text('Dashboard'), findsOneWidget);

      // 6. Logout
      await tester.tap(find.byIcon(Icons.logout));
      await tester.pumpAndSettle();

      // 7. Verify back to home
      expect(find.text('Welcome'), findsOneWidget);
    });

    testWidgets('navigation test', (WidgetTester tester) async {
      app.main();
      await tester.pumpAndSettle();

      // Navigate through multiple screens
      await tester.tap(find.text('Settings'));
      await tester.pumpAndSettle();
      expect(find.text('Settings Page'), findsOneWidget);

      await tester.tap(find.byIcon(Icons.arrow_back));
      await tester.pumpAndSettle();
      expect(find.text('Welcome'), findsOneWidget);
    });
  });
}

Running Integration Tests

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

# On specific device
flutter test integration_test/app_test.dart -d <device_id>

# With debugging
flutter drive   --driver=test_driver/integration_test.dart   --target=integration_test/app_test.dart

Comparison Table

FeatureUnit TestWidget TestIntegration Test
TestsFunctions, classesUI widgetsFull app flow
SpeedVery fastFastSlow
DependenciesIsolated (mocked)Mocked servicesReal services
Runs onLocal machineLocal machineDevice/emulator
CoverageBusiness logicUI componentsEnd-to-end
Use caseValidate calculationsValidate UIValidate user flows
Package
text
flutter_test
text
flutter_test
text
integration_test

Test Pyramid

text
         /        /  \  Integration Tests (Few, Slow, Expensive)
       /----      /      \  Widget Tests (Moderate, Moderate Speed)
     /--------    /__________\  Unit Tests (Many, Fast, Cheap)

Best Practice: Write more unit tests, fewer integration tests.


Additional Test Types

4. Golden Tests (Snapshot Testing)

Test that UI renders exactly as expected by comparing screenshots.

dart
testWidgets('golden test', (WidgetTester tester) async {
  await tester.pumpWidget(MyWidget());
  await expectLater(
    find.byType(MyWidget),
    matchesGoldenFile('goldens/my_widget.png'),
  );
});

5. Performance Testing

dart
import 'package:flutter_driver/flutter_driver.dart';

void main() {
  group('Performance Test', () {
    late FlutterDriver driver;

    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    tearDownAll(() async {
      driver.close();
    });

    test('scroll performance', () async {
      final timeline = await driver.traceAction(() async {
        await driver.scroll(
          find.byType('ListView'),
          0,
          -500,
          Duration(milliseconds: 300),
        );
      });

      final summary = TimelineSummary.summarize(timeline);
      summary.writeSummaryToFile('scroll_performance', pretty: true);
    });
  });
}

Best Practices

PracticeRecommendation
Test coverageAim for 80%+ code coverage
Test namingUse descriptive names:
text
test('should return user when valid ID provided')
setUp/tearDownUse for common initialization/cleanup
Mock dependenciesUse
text
mockito
package for mocking
Organize testsMirror lib/ structure in test/
Run frequentlyRun tests before every commit

Running All Tests

bash
# Run all tests
flutter test

# Run with coverage
flutter test --coverage

# Run specific file
flutter test test/my_test.dart

# Run tests in watch mode
flutter test --watch

Resources