DEV Community

mobisoftinfotech
mobisoftinfotech

Posted on

Flutter Testing: Unit, Widget & Integration Tests Guide

Image description
Flutter has gained immense popularity for its capability to build cross-platform apps from a single codebase. A crucial part of Flutter app development is Flutter testing, which ensures that your application performs as expected. In this blog, we’ll delve into the three primary types of testing in Flutter: Unit testing in Flutter, Widget testing in Flutter, and Integration testing in Flutter, and explore how to write each effectively.

Why Testing is Important in Flutter

Flutter test automation plays a key role in ensuring smooth development cycles. Testing helps identify bugs early, enhances code maintainability, and improves user experience. By implementing robust Flutter test cases, developers can:

  • Prevent regressions
  • Reduce manual testing efforts
  • Ensure smoother app performance and reliability

Flutter offers a rich set of tools and libraries for Flutter test framework integration, making it easier to maintain a high standard of code quality.

Types of Flutter Tests

Image description

Flutter Unit Testing:

  • Unit tests verify individual functions, methods, or classes in isolation. Example: Testing a simple Cart function.

Flutter Integration Testing:

  • Integration tests validate the complete app or a large part of it with actual UI interactions. Example: Testing a Login screen.

Image description

1. What is Flutter Unit Testing?

Unit testing in Flutter verifies individual units of code (typically methods or functions) to ensure they work as expected. It is the smallest form of testing and helps catch bugs early in development.

Setting Up a Flutter Unit Test

Before writing tests, add the Flutter testing dependency to your pubspec.yaml file:

Image description
Then, run flutter pub get to fetch the package.

Writing Your First Unit Test

Let’s assume we have a simple CartItemclass:

class CartItem {
  final String name;
  final double price;
  final int quantity;
  CartItem({required this.name, required this.price, required this.quantity});
  double get total => price * quantity;
}
class ShoppingCart {
  final List<CartItem> _items = [];
  List<CartItem> get items => List.unmodifiable(_items);
  void addItem(CartItem item) {
    _items.add(item);
  }
  void removeItem(String name) {
    _items.removeWhere((item) => item.name == name);
  }
  double calculateTotal() {
    return _items.fold(0.0, (total, item) => total + item.total);
  }
  void clearCart() {
    _items.clear();
  }
}
Code language: JavaScript (javascript)

Enter fullscreen mode Exit fullscreen mode

To test this class, create a new file cart_item_test.dart under test/:

import 'package:flutter_test/flutter_test.dart';
import 'package:fluttertest/cart_item.dart';

void main() {

late ShoppingCart cart;
  setUp(() {
    cart = ShoppingCart();
  });

  test('Add item to cart', () {
    final item = CartItem(name: 'Laptop', price: 1000.0, quantity: 1);
    cart.addItem(item);
    expect(cart.items.length, 1);
    expect(cart.items.first.name, 'Laptop');
    expect(cart.items.first.price, 1000.0);
    expect(cart.items.first.quantity, 1);
  });

  test('Remove item from cart', () {
    final item = CartItem(name: 'Laptop', price: 1000.0, quantity: 1);
    cart.addItem(item);
    cart.removeItem('Laptop');
    expect(cart.items.isEmpty, true);
  });

  test('Calculate total price of items in cart', () {
    final item1 = CartItem(name: 'Laptop', price: 1000.0, quantity: 1);
    final item2 = CartItem(name: 'Phone', price: 500.0, quantity: 2);
    cart.addItem(item1);
    cart.addItem(item2);
    expect(cart.calculateTotal(), 2000.0);
  });

  test('Clear cart', () {
    final item1 = CartItem(name: 'Laptop', price: 1000.0, quantity: 1);
    final item2 = CartItem(name: 'Phone', price: 500.0, quantity: 2);
    cart.addItem(item1);
    cart.addItem(item2);
    cart.clearCart();
    expect(cart.items.isEmpty, true);
  });
}
Enter fullscreen mode Exit fullscreen mode

Key Components of Flutter Unit Tests

  • test():Defines a Flutter test case.
  • expect(): Compares actual output with expected output.

Explanation

Test 1 – Add Item to Cart

_Purpose: _This test ensures that the addItem() method functions correctly and that the cart is updated when a new item is added.
Steps:

  • Create a CartItemnamed “Laptop” with a price of 1000.0 and quantity of 1.
  • Call cart.addItem(item) to add the item to the shopping cart.
  • Use expect() to assert that the cart:

  • Contains 1 item (cart.items.length)

  • The first item in the cart has the name “Laptop”

  • The price of the first item is 1000.0

  • The quantity of the first item is 1

Test 2 – Remove Item from Cart

Purpose: This test ensures that the removeItem() method removes the correct item from the cart.
Steps:

  • Create a CartItemnamed “Laptop” and add it to the cart.
  • Call cart.removeItem('Laptop') to remove the item.
  • Use expect() to assert that the cart is empty (cart.items.isEmpty is true), confirming the item was successfully removed.

Test 3 – Calculate Total Price of Items in Cart

Purpose: This test checks if the calculateTotal() method correctly calculates the total price for all items in the cart.
Steps:

  • Create two items: “Laptop” (price 1000.0, quantity 1) and “Phone” (price 500.0, quantity 2).
  • Add both items to the cart using cart.addItem().
  • Call cart.calculateTotal() to get the total price.
  • Use expect() to assert that the total price is 2000.0 (since 1000 + 500*2 = 2000).

Test 4 – Clear Cart

Purpose: This test ensures that the clearCart() method successfully removes all items from the cart.
Steps:

  • Create two items: “Laptop” and “Phone”, and add them to the cart.
  • Call cart.clearCart() to remove all items.
  • Use expect() to assert that the cart is empty (cart.items.isEmpty is true).

By testing each of these functions, we ensure that the cart behaves as expected, handling typical use cases like adding, removing, and totaling items, as well as clearing the cart. Unit tests like these help catch potential issues early, ensuring robust and reliable business logic.

These tests are important for any real-world application, especially e-commerce apps where accuracy in cart operations directly impacts user experience and functionality.

Run Tests and result:

Command – flutter test test/cart_item_test.dart

Result:

Image description

2. What is Flutter Widget Testing?

Flutter widget testing is used to test individual UI components (widgets) in isolation, ensuring they behave as expected. These tests are one level above unit testing in Flutter and focus on interactions, rendering, and layout of the widget tree without requiring a full app running environment or external services.

Let’s take an example of a contact screen:

import 'package:flutter/material.dart';
void main() {
  runApp(const MyApp());
}
class ContactScreen extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: ContactForm(),
    );
  }
}
class ContactForm extends StatefulWidget {
  const ContactForm({super.key});
  @override
  _ContactFormState createState() => _ContactFormState();
}
class _ContactFormState extends State<ContactForm> {
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  final _messageController = TextEditingController();
  void _submitForm() {
    // Simply show a snackbar upon submission
    ScaffoldMessenger.of(context)
        .showSnackBar(const SnackBar(content: Text('Form Submitted')));
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Contact Form')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            TextField(
              controller: _nameController,
              decoration: const InputDecoration(labelText: 'Name'),
            ),
            TextField(
              controller: _emailController,
              decoration: const InputDecoration(labelText: 'Email'),
              keyboardType: TextInputType.emailAddress,
            ),
            TextField(
              controller: _messageController,
              decoration: const InputDecoration(labelText: 'Message'),
              maxLines: 3,
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _submitForm,
              child: const Text('Submit'),
            ),
          ],
        ),
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Writing the Widget Test

Create a contact_screen_test.dart in your test/ directory.

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

void main() {

    testWidgets('Contact Form UI - Submit button test',
      (WidgetTester tester) async {

   // Build the ContactForm widget
    await tester.pumpWidget(const MaterialApp(home: ContactForm()));

    // Find the text fields and submit button
    final nameField = find.byType(TextField).first;
    final emailField = find.byType(TextField).at(1);
    final messageField = find.byType(TextField).at(2);
    final submitButton = find.text('Submit');

   // Verify if the text fields are present
    expect(nameField, findsOneWidget);
    expect(emailField, findsOneWidget);
    expect(messageField, findsOneWidget);

   // Verify if the submit button is present
    expect(submitButton, findsOneWidget);

     // Enter text into the fields
    await tester.enterText(nameField, 'John Doe');
    await tester.enterText(emailField, 'john.doe@example.com');
    await tester.enterText(
        messageField, 'Hello! I would like to inquire about...');

   // Tap the submit button
    await tester.tap(submitButton);
    await tester.pump();
  });
}

Enter fullscreen mode Exit fullscreen mode

Explanation

  • Find Widgets: Use find.byKey, find.text, or find.byType to locate UI elements.
  • Interact with Widgets: Use enterText, tap, etc.
  • Rebuild UI: await tester.pump() simulates a frame being drawn after a state change.
  • Expectations: Check UI outputs using expect.

Run the Tests:

Use the following command in your terminal to run the widget test:

flutter test test/contact_screen_test.dart
This will execute the test cases and provide a report of the results.

Result:

Image description

Explanation:

  1. Building the Widget: await tester.pumpWidget(MaterialApp(home: ContactForm())); initializes the ContactForm widget inside a MaterialApp.

  2. Finding Widgets:

  • find.byType(TextField) is used to locate the three TextFieldwidgets (for name, email, and message).
  • find.text('Submit') locates the submit button.
  • Verifying Widget Presence:

  • expect(nameField, findsOneWidget) checks if the text fields are rendered on the screen.

  • expect(submitButton, findsOneWidget) checks if the submit button is rendered.

  • Simulating User Input:

  • await tester.enterText(nameField, 'John Doe'); simulates typing into the name field.

  • Similarly for the email and message fields.

  • Tapping the Button:

  • await tester.tap(submitButton);simulates a tap on the submit button.

  • await tester.pump(); triggers a re-render to reflect the updated state after the tap.

3. What is Flutter Integration Testing?

Flutter integration testing verifies the entire app’s functionality by testing multiple widgets, screens, and external dependencies working together. These tests simulate real-world usage and are more complex, as they often involve interacting with the real UI, API calls, and databases.

To set up integration tests:

Flutter has a package specifically for integration testing: integration_test. You can also run these tests on an actual device or emulator.

Add the Required Dependencies

To get started, you need to add the integration_test and flutter_test packages to your pubspec.yaml file. If you’re working with Firebase or other back-end services, you might also need to include specific dependencies for mocking or interacting with them.

Image description

Write an Example Integration Test

Now let’s write a simple integration test for the above login screen.

Below integration test will cover all the following scenarios:

  • Successful login: When the correct email and password are entered, the login should be successful.
  • Failed login due to incorrect credentials: When the wrong email or password is entered, the login should fail.
  • Empty fields: Ensure that both fields are validated (even though your code does not explicitly validate, we can test that pressing the button with empty fields results in failure).
  • UI behavior: Check that the UI components are displayed correctly, such as the TextFieldwidgets and ElevatedButton. You can place this test in your integration_test directory.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fluttertest/login_screen.dart';
import 'package:integration_test/integration_test.dart';
void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  group('LoginScreen Integration Test', () {
    testWidgets('Successful login scenario', (tester) async {
      // Pump the LoginScreen widget into the widget tree
      await tester.pumpWidget(MaterialApp(home: LoginScreen()));
      // Enter correct credentials
      await tester.enterText(
          find.byKey(const Key('emailField')), 'test@example.com');
      await tester.enterText(
          find.byKey(const Key('passwordField')), 'password123');
      // Tap the login button
      await tester.tap(find.byKey(const Key('loginButton')));
      await tester.pumpAndSettle();
      // Verify the "Login Successful" message is shown
      expect(find.text('Login Successful'), findsOneWidget);
    });
    testWidgets('Failed login with incorrect credentials', (tester) async {
      // Pump the LoginScreen widget into the widget tree
      await tester.pumpWidget(MaterialApp(home: LoginScreen()));
      // Enter incorrect credentials
      await tester.enterText(
          find.byKey(const Key('emailField')), 'wrong@example.com');
      await tester.enterText(
          find.byKey(const Key('passwordField')), 'wrongpassword');
      // Tap the login button
      await tester.tap(find.byKey(const Key('loginButton')));
      await tester.pumpAndSettle();
      // Verify the "Login Failed" message is shown
      expect(find.text('Login Failed'), findsOneWidget);
    });
    testWidgets('Login attempt with empty fields', (tester) async {
      // Pump the LoginScreen widget into the widget tree
      await tester.pumpWidget(MaterialApp(home: LoginScreen()));
      // Leave both fields empty
      await tester.enterText(find.byKey(const Key('emailField')), '');
      await tester.enterText(find.byKey(const Key('passwordField')), '');
      // Tap the login button
      await tester.tap(find.byKey(const Key('loginButton')));
      await tester.pumpAndSettle();
      // Verify the "Login Failed" message is shown (since we expect no credentials entered)
      expect(find.text('Login Failed'), findsOneWidget);
    });
    testWidgets('UI elements are present', (tester) async {
      // Pump the LoginScreen widget into the widget tree
      await tester.pumpWidget(MaterialApp(home: LoginScreen()));
      // Verify the email and password fields are present
      expect(find.byKey(const Key('emailField')), findsOneWidget);
      expect(find.byKey(const Key('passwordField')), findsOneWidget);
      // Verify the login button is present
      expect(find.byKey(const Key('loginButton')), findsOneWidget);
    });
  });
}

Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. Successful login:

The test enters the correct email and password (test@example.com and password123), taps the login button, and expects the SnackBar message “Login Successful”.

  1. Failed login due to incorrect credentials:

This test enters incorrect credentials (wrong@example.com and wrongpassword) and expects the “Login Failed” message in the SnackBar.

  1. Empty fields:

This test verifies that if the email and password fields are left empty, tapping the login button results in the “Login Failed” message. This assumes the empty field case results in failure, as your code does not have validation for empty fields, but it could be something you’d like to add.

  1. UI behavior:

This test checks that the key elements (the email field, password field, and login button) are properly displayed on the screen.

Running the Integration Test:

Command – flutter test integration_test/login_screen_test.dart

Result:

Conclusion

Flutter automated testing is a vital aspect of building reliable and maintainable Flutter applications. By leveraging unit testing in Flutter, widget testing, and Flutter integration tests, you can ensure that each piece of your app functions as expected and integrates seamlessly with other components. Flutter’s powerful testing tools make it easier than ever to implement a comprehensive testing strategy for your apps.

To download the source code for the sample app, click here.

Image description

Source Link : Flutter Testing: Unit, Widget & Integration Tests Guide

Top comments (0)