Mastering Testing Techniques in Flutter

Delve deep into the world of Flutter testing with this comprehensive overview. 

Uncover the various testing techniques, including unit testing, widget testing, integration testing, and end-to-end testing, and grasp their significance in ensuring the robustness and excellence of your Flutter applications. 


Whether you're new to Flutter or an experienced developer, this blog post equips you with the knowledge and resources to create flawless Flutter apps through effective testing methodologies.

  • Unit Testing
  • Widget Testing
  • Integration Testing
  • End-to-End (E2E) Testing
  • Golden Testing
  • Performance Testing
  • Accessibility Testing
  • Security Testing
  • Cloud Firebase Test Lab - Google


Unit Testing

Unit testing in Flutter involves testing individual units of code, such as functions, methods, or classes, in isolation from the rest of the application. The goal is to verify that each unit of code works correctly.

Tools: Flutter's built-in test package, mockito for mocking, and other testing libraries.

Use Cases: Testing pure Dart logic, business logic, and utility functions.

Code:

lib/calculator.dart


class Calculator {
  // This class represents a simple calculator.

  // The `add` method takes two integers `a` and `b` as input and returns their sum.
  int? add(int a, int b) {
    return a + b; // Calculate the sum of `a` and `b` and return it.
  }
}

..

Normal Testing

Your code has simple dependencies with known and predictable behavior. 


test/calculator_test.dart


import 'package:flutter_test/flutter_test.dart';
import 'package:testing_demo/calculator.dart';

void main() {
  Calculator? cal; // Declare a reference to the Calculator class

  setUpAll(() {
    cal =
        Calculator(); // Create an instance of the Calculator class before running tests
  });

  group('Add function tests', () {
    test('Adding two positive numbers', () {
      // Arrange: Prepare the inputs
      // Act: Call the add function
      // Assert: Verify the result
      expect(cal?.add(3, 5), equals(8));
    });

    test('Adding a positive number and zero', () {
      expect(cal?.add(3, 0), equals(3));
    });

    test('Adding a negative number and a positive number', () {
      expect(cal?.add(-3, 5), equals(2));
    });

    test('Adding two negative numbers', () {
      expect(cal?.add(-3, -5), equals(-8));
    });

    tearDownAll(() {
      // This block runs after all the tests in this group have finished.
      // You can perform cleanup tasks here if needed.
      print('All tests are done');
    });
  });
}

..

Terminal: 

 flutter test test/calculator_test.dart

..

Use Mock Testing when:

Your code has complex dependencies, external services, or APIs that you want to simulate.

Need to add the mockito package


dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^5.0.7

..

test/calculator_mock_test.dart


import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:testing_demo/calculator.dart';

class MockCalculator extends Mock implements Calculator {}

void main() {
  late MockCalculator mockCalculator;

  setUpAll(() {
    mockCalculator = MockCalculator();
  });


  test('Test adding two numbers', () {
    // Define the behavior of the mock
    when(mockCalculator.add(3, 5)).thenReturn(8);

    // Your test logic that uses the Calculator class with the mocked dependency
    int? result = mockCalculator.add(3, 5);

    // Verify that the mock was called with specific arguments
    verify(mockCalculator.add(3, 5)).called(1);

    // Verify the result
    expect(result, equals(8));
  });


  
  tearDown(() {
    // Clean up after each test if necessary
  });
}

..

  • when: This function is used to specify the behavior of a mock object when a certain method is called with specific arguments. 
  • thenReturn: This function is used in conjunction with when to specify the return value of a mock method when it's called with certain arguments. 
  • verify: This function is used to verify that a specific method on a mock object was called with certain arguments and a specific number of times
  • called: This is a function used with verify to specify the number of times a method should have been called. 
  • expect: This function is used to make assertions in your test. You can use it to check if a certain value matches an expected value. It's commonly used to assert the results of method calls on your code under test.


..

Widget Testing

Widget testing in Flutter is focused on testing the UI components (widgets) of your app. It allows you to verify how widgets are rendered and interact with each other.

Tools: flutter_test package and WidgetTester class.

Use Cases: Testing widget behavior, layout, and interactions. It provides a higher level of confidence in the UI.

lib/main.dart


import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
   MyHomePageState createState() => MyHomePageState();
}

class MyHomePageState extends State {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Counter App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'Counter:',
              key: Key('counter_text'), // Key for integration testing
            ),
            Text(
              '$_counter',
              style: const TextStyle(fontSize: 24),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              key: const Key('increment_button'), // Key for integration testing
              onPressed: _incrementCounter,
              child: const Icon(Icons.add),
            ),
          ],
        ),
      ),
    );
  }
}


test/widget_test.dart


// This is a basic Flutter widget test.  
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:testing_demo/main.dart';

void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    // Build our app and trigger a frame.
    await tester.pumpWidget(const MyApp());

    // Verify that our counter starts at 0.
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Tap the '+' icon and trigger a frame.
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    // Verify that our counter has incremented.
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });
}

..

  • void main(): This is the entry point of your test file. It's where your test suite starts executing.
  • testWidgets(): This function is used to define a test case for a widget. It takes a callback function as an argument, which contains the actual test logic.
  • (WidgetTester tester) async { ... }: This is an anonymous function that defines the test case. It takes a WidgetTester object as a parameter, which allows you to interact with and test widgets.
  • await tester.pumpWidget(const MyApp());: This line builds your Flutter app (in this case, the MyApp widget) and triggers a frame to update the widget tree.
  • expect(find.text('0'), findsOneWidget);: This line uses the expect function to assert that there's exactly one widget with the text '0' in the widget tree. It checks if the initial state of the counter is 0.
  • expect(find.text('1'), findsNothing);: This line asserts that there's no widget with the text '1' in the widget tree at this point.
  • await tester.tap(find.byIcon(Icons.add));: This simulates tapping on a widget that has the 'add' icon (in this case, the '+' icon).
  • await tester.pump();: After tapping the icon, this line triggers another frame to update the widget tree.
  • expect(find.text('0'), findsNothing);: This asserts that the widget with the text '0' is no longer present in the widget tree, indicating that the counter has been incremented.
  • expect(find.text('1'), findsOneWidget);: This asserts that there's now one widget with the text '1' in the widget tree, confirming that the counter has incremented.



Integration Testing

Integration testing in Flutter involves testing the interaction between different parts or modules of your app. It verifies that various components work together as expected.

Tools: Flutter's integration_test package and flutter drive command for end-to-end testing.

Use Cases: Testing the interaction between UI components, navigation, and data flow.

project/integration_test/integration_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:testing_demo/main.dart' as app;
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('Test the entire app flow', (WidgetTester tester) async {
    app.main(); // Start your app

    await tester.pumpAndSettle(); // Wait for your app to settle

    // Perform interactions and assertions here
    // Example: Tap a button, expect a result
    await tester.tap(find.byKey(const Key('increment_button')));
    await tester.pumpAndSettle();
    expect(find.text('1'), findsOneWidget);
  });
}
Terminal: flutter test integration_test
Read more : https://docs.flutter.dev/testing/integration-tests
..



End-to-End (E2E) Testing:

End-to-end testing in Flutter checks the functionality of your app as a whole by simulating user interactions and verifying the app's behavior.
Tools: Flutter's integration_test package and flutter drive command.
Use Cases: Testing user flows, scenarios, and critical paths in your app.


Golden Testing:
Golden testing is a type of widget testing used for visual regression testing. It captures the visual representation of widgets as images (golden files) and compares them to detect visual changes.
Tools: flutter_test package with golden file support.
Use Cases: Ensuring that UI components maintain their appearance over time and across different Flutter versions.


Performance Testing:
Performance testing evaluates the performance characteristics of your Flutter app, such as CPU usage, memory usage, and response times, to ensure it meets performance requirements.
Tools: Flutter's built-in tools and libraries, such as the flutter_driver package for performance profiling.
Use Cases: Identifying performance bottlenecks and optimizing your app for smooth operation.

Accessibility Testing:
Accessibility testing focuses on ensuring that your app is accessible to users with disabilities. It checks if your app adheres to accessibility guidelines and standards.
Tools: Flutter's built-in accessibility features and manual testing with accessibility tools.
Use Cases: Ensuring that your app can be used by individuals with disabilities, including screen readers and voice input.


Security Testing:
Security testing aims to identify vulnerabilities and security risks in your Flutter app. It includes various testing techniques like static analysis, dynamic analysis, and penetration testing.
Tools: Security testing tools and best practices for mobile app security.
Use Cases: Protecting sensitive user data and preventing security breaches.

Comments