Flutter testing

This section will introduce three types of testing in Flutter: unit tests, widget tests, and integration tests.


Test Types Overview

TypeDescriptionRun commands
Unit TestingTesting individual functions and class methodsflutter test
Widget TestingTest the UI of a single Widgetflutter test
Integration TestingTesting the complete application flowflutter test integration_test/

Unit Testing

Unit tests are used to test business logic code.

Example: Unit Test Example

// math_utils.dart - code to be tested
class MathUtils {
  static int add(int a, int b) => a + b;
  static int subtract(int a, int b) => a - b;
  static int multiply(int a, int b) => a * b;
  static double divide(int a, int b) {
    if (b == 0) throw ArgumentError(Cannot divide by zero);
    return a / b;
  }

  static int factorial(int n) {
    if (n < 0) throw ArgumentError(Negative numbers do not have factorials);
    if (n == 0 || n == 1) return 1;
    return n * factorial(n - 1);
  }
}

// math_utils_test.dart - test file
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/math_utils.dart';

void main() {
  group('MathUtils', () {
    test(add adds two numbers, () {
      expect(MathUtils.add(2, 3), equals(5));
    });

    test(subtract subtracts two numbers, () {
      expect(MathUtils.subtract(5, 3), equals(2));
    });

    test(multiply multiplies two numbers, () {
      expect(MathUtils.multiply(4, 5), equals(20));
    });

    test(divide divides two numbers, () {
      expect(MathUtils.divide(10, 2), equals(5));
    });

    test(divide throws exception when dividing by zero, () {
      expect(() => MathUtils.divide(10, 0), throwsArgumentError);
    });

    test(factorial calculates factorial, () {
      expect(MathUtils.factorial(5), equals(120));
      expect(MathUtils.factorial(0), equals(1));
    });

    test(factorial throws exception for negative numbers, () {
      expect(() => MathUtils.factorial(-1), throwsArgumentError);
    });
  });
}

Widget Testing

Widget tests are used to test UI components.

Example: Widget Test

// counter_widget.dart - Widget under test
class CounterWidget extends StatefulWidget {
  const CounterWidget({super.key});

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

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

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Text(Count: $_count, key: const Key('counter_text')),
        ElevatedButton(
          key: const Key('increment_button'),
          onPressed: _increment,
          child: const Text('Increase'),
        ),
      ],
    );
  }
}

// counter_widget_test.dart - test file
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/counter_widget.dart';

void main() {
  testWidgets(CounterWidget displays initial count, (WidgetTester tester) async {
    // Build Widget
    await tester.pumpWidget(const MaterialApp(
      home: CounterWidget(),
    ));

    // Verify initial count display
    expect(find.text(Count: 0), findsOneWidget);
  });

  testWidgets(CounterWidget increments count on button click, (WidgetTester tester) async {
    await tester.pumpWidget(const MaterialApp(
      home: CounterWidget(),
    ));

    // Find and click the button
    await tester.tap(find.byKey(const Key('increment_button')));
    await tester.pump();

    // Verify count has been updated
    expect(find.text(Count: 1), findsOneWidget);

    // Click again
    await tester.tap(find.byKey(const Key('increment_button')));
    await tester.pump();

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

Integration Testing

Integration tests are used to test the complete application functionality flow.

Example: Integration Test

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

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group(App integration test, () {
    testWidgets(Full user flow test, (WidgetTester tester) async {
      // Launch app
      await tester.pumpWidget(const MyApp());
      await tester.pumpAndSettle();

      // Verify home page is displayed
      expect(find.text('Home'), findsOneWidget);

      // Click to navigate to the detail page
      await tester.tap(find.text('Details'));
      await tester.pumpAndSettle();

      // Verify detail page display
      expect(find.text('Detail Page'), findsOneWidget);

      // Click back
      await tester.tap(find.byIcon(Icons.arrow_back));
      await tester.pumpAndSettle();

      // Verify return to home
      expect(find.text('Home'), findsOneWidget);
    });
  });
}

Good test coverage can improve code quality; it is recommended to write unit tests for at least the core business logic.

other extensions