Testing in Flutter Made Simple: The Bicycle Example

Testing in Flutter Made Simple: The Bicycle Example

Introduction

Okay, so let's say you want to build a bicycle, and you want to make sure your bicycle really works.

Starting with the Basics

You want to build a good bicycle, maybe for a competition. In order to build a bicycle, there are multiple parts to it. You have to make sure the bicycle's functionalities work properly.

For example:

  • Pressing the brakes should stop the bicycle.

  • Pedaling should make the wheels spin.

These are individual functionalities, and you have to make sure they really work well.


Unit Testing: Ensuring Individual Functionalities Work

Unit testing is all about verifying that individual functionalities, like pressing the brakes or pedaling, perform as expected.

For instance: Pressing the brakes should stop the bicycle.


Widget Testing: Combining and Testing Together

After you’ve ensured the brakes and pedals work individually, you combine all the parts together with a nice bicycle frame.

  • Testing the combined functionality in a controlled environment (e.g., your garage) is called widget testing.

Integration Testing: Testing in Real Environments

Once your bicycle works well in the garage, you take it outdoors to test it on different terrains.

  • Testing in real-world environments (roads, wild terrains, etc.) is called integration testing.

Widget testing is like testing in a closed environment (e.g., a garage), while integration testing is about testing in actual environments.


Why Testing is Crucial

Testing is the most crucial part of building a bicycle (or an app). It ensures everything works before the competition (or launch).

The Problem with Manual Testing

While manual testing works for simple apps, it becomes impractical as your app grows.

  • Imagine publishing your app on the Apple and Google Play Store, and it becomes popular with millions of users.

  • With multiple developers working on the app, mistakes can be introduced, and identifying issues manually becomes costly.

The Solution: Automated Testing

With unit and widget tests, you can:

  1. Identify which part of the app isn’t working.

  2. Fix it quickly and efficiently.


How to Do Testing in Flutter

1. Unit Testing

In Flutter, unit testing involves testing individual functions, just like testing the brakes of a bicycle.

Example: Testing Brakes

Let’s assume we have a function called brakes(), which prints "Bicycle stopped".

Here’s how you test it:

import 'package:flutter_test/flutter_test.dart';

void main() {
  test('Testing the brakes of the bicycle', () {
    // Call the brakes function
    String result = brakes();

    // Expect the result to be "Bicycle stopped"
    expect(result, 'Bicycle stopped');
  });
}

// Example function
String brakes() {
  return 'Bicycle stopped';
}

Testing Network Calls with Mocks

In apps, we usually have network calls, like fetching your Instagram posts or messages when you open Instagram. Typically, we would test the app manually to see if the data is fetching properly. However, when we are doing unit testing, we are not compiling and running the app, are we? So, how are we going to fake (mock) the network call? Luckily, we have our superhero called Mockito, which, as its name suggests, helps us to mock the network call.

Steps:

  1. Use Dependency Injection to mock dependencies. see previous articles to understand it .

  2. Use the Mockito library for mocking.

Here, we create our superhero by making its object: final mockRequest = MockRequest();

We can easily mock the data by specifying that "when" we call the network request, we want it to return "Success."

    when(mockRequest.get('url')).thenAnswer(
      (_) async => Response(data: 'Success', statusCode: 200),
    );

Example: Mocking a Network Request

import 'package:mockito/mockito.dart';

class MockRequest extends Mock implements Dio {}

void main() {
  test('Mocking network request', () {
    // Create a mock instance
    final mockRequest = MockRequest();

    // Mock the response
    when(mockRequest.get('url')).thenAnswer(
      (_) async => Response(data: 'Success', statusCode: 200),
    );

    // Test the response
    expect(mockRequest.get('url').then((res) => res.data), completion('Success'));
  });
}

2. Widget Testing

Widget testing ensures that the entire widget works as expected, just like testing the complete bicycle in your garage.

Example: Testing the Bicycle Widget

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

void main() {
  testWidgets('Testing the brakes in a widget', (WidgetTester tester) async {
    // Create the widget (bicycle)
    await tester.pumpWidget(Bicycle());

    // Find the brakes
    final brakes = find.byType(Brake);

    // Tap the brakes
    await tester.tap(brakes);

    // Rebuild the widget
    await tester.pump();

    // Expect the bicycle to stop
    expect(find.text('Bicycle stopped'), findsOneWidget);
  });
}

// Example widget
class Bicycle extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            Text('Bicycle stopped'),
            ElevatedButton(onPressed: () {}, child: Text('Brake')),
          ],
        ),
      ),
    );
  }
}

Let’s bring the bicycle analogy back to understand integration testing.

You’ve already made sure that:

  1. The individual functionalities (like brakes and pedals) work properly through unit testing.

  2. The entire bicycle works when put together in a controlled environment, like your garage, through widget testing.

But now, you want to take the bicycle out of the garage and test it in the real world—on actual roads, terrains, and weather conditions.


What Does Integration Testing Do?

In integration testing, you’re making sure that:

  • All the parts of the bicycle (brakes, pedals, frame, chain, etc.) work together.

  • The bicycle functions properly in real-life scenarios, like on bumpy roads, uphill climbs, or slippery surfaces.

  • External factors, like different terrains and weather conditions, don’t cause the bicycle to break down.

For example:

  • If someone rides the bicycle and presses the brake while going downhill, does it stop?

  • If they pedal hard on uneven ground, do the wheels still spin?


Integration Testing in Flutter: Bicycle Example

Let’s say we’ve built a bicycle app. This app has multiple widgets (like the brakes and pedals), and it fetches data from a server (such as the current road condition).

In integration testing, we simulate the real-world experience by testing the app as a whole.


Flutter Code: Integration Testing the Bicycle

Here’s how you can write an integration test for our bicycle analogy:

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

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('Test the bicycle app in real-world scenarios', (WidgetTester tester) async {
    // Simulating the bicycle app
    var app = MaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            ElevatedButton(
              onPressed: () => print("Brakes applied"),
              child: Text('Press Brakes'),
            ),
            ElevatedButton(
              onPressed: () => print("Pedals spun"),
              child: Text('Start Pedaling'),
            ),
            Text('No terrain detected', key: Key('terrain-text')),
          ],
        ),
      ),
    );

    // Pump the app into the test environment
    await tester.pumpWidget(app);

    // Simulate pressing the brakes
    var brakeButton = find.text('Press Brakes');
    await tester.tap(brakeButton);
    await tester.pump();

    // Simulate pedaling
    var pedalButton = find.text('Start Pedaling');
    await tester.tap(pedalButton);
    await tester.pump();

    // Simulate retrieving terrain data (mocking a network call)
    var terrainText = find.byKey(Key('terrain-text'));

    // Assert that the terrain data is displayed
    expect(terrainText, findsOneWidget);
  });
}

Integration Testing: Real-World Bicycle Testing

In this test:

  • We simulated pressing the brakes and ensured the bicycle stops.

  • We simulated pedaling and ensured the wheels spin.

  • We also checked if the bicycle can retrieve terrain data (e.g., "road is bumpy").


Key Points of the Analogy

  1. Unit Testing: Each part of the bicycle (brakes, pedals) is tested separately to ensure it works.

  2. Widget Testing: The entire bicycle is tested in a controlled environment (your garage).

  3. Integration Testing: The bicycle is taken out into the real world (roads and terrains) to test everything working together.


By testing your app with integration tests, you ensure that:

  • All individual parts (unit-tested components) and assembled widgets (widget-tested) work together.

  • Your app functions as expected in real-life use cases.

This gives you confidence that your "bicycle" (or app) is ready for the "competition" (production). 🚴‍♂️

Conclusion

Testing is vital to ensure your app works reliably, especially as it grows in complexity. In Flutter:

  • Unit tests test individual functions.

  • Widget tests validate widgets in isolation.

  • Integration tests ensure the app works in real-world scenarios.

By incorporating these tests into your development process, you can build robust and maintainable apps.