Getting Started with Test Writing in Flutter for Newbies

RobinRobin
9 min read

On my journey of becoming a junior developer, i started a lot of different side projects. For each and everyone of them i had the dream that it would eventually become a huge success an make me the next zuckerberg (avarege junior dev thinking). That is why i always thought that at some point in the development process i would implement automatic test, to ensure my application would run good. My mentality regarding these tests was something like this: “implement the basic functionality of your app, so you have something, and implement tests later.” And you might have guest it, those tests never came.

The problem was that i did not know how to implement test. I thought that as soon as this magical point in time would come, i would just simply learn how to write those tests and implement them in to my application. But because that did not work, i decided to take a different approach. I currently want to learn flutter, because i have a cool idea for a time management/gamification app, that i want to develop. This time i will learn how to implement test, before i even start writing my application and even before i dive deeper into how flutter works, to ensure that i can implement tests as soon as i start developing.

So in this article i will take you with me on the quest of testing in flutter.

Why tests?

Automatic testing is a big part of software development. Testing can help to prevent bugs. By testing small pieces of codes, you can identify issue before they grow into larger and more complex problems, that are just a pain in the ass to debug. Once you have tests, debugging also gets easier, because you most likely know whats breaking, and where. For me the coolest part is the fact that i can rewrite or refactor my code and can be sure that everything still runs afterwards, as long as the original tests are still passing. It is also a way of documenting your code, because you can read the desired behavior just of the test, you can change something and can be sure that it did not break anything else, you don’t have to test as much manually, it has cool applications for Continuous Integration (no idea what exactly that is but chatgpt said that and it sounds cool) and the most important part: it allows TDD (Test Driven Development).

TDD ist the practice of writing test, that describe your desired behaviour, before you even start to implement that behaviour. That is exactly the approach that i want to go with.

What tests?

There a 3 different types of tests that i want to cover in this article:

  • Unit tests

  • Widget tests

  • Integration tests

Unit Tests

A Unit test tests a single function. The goal of a Unit test is to make sure that your logic works under a variety of different conditions (i.e. is your code dying, as soon as something unexpected is handed over to your function)

Widget tests

A widget test (in other UI frameworks referred to as component test) tests a single widget. The goal of a widget test is to verify that the widget looks and interacts as expected.

Integration Tests

an Integration test tests your complete app or a large part of your app at ones. An integration test should make you sure that the different parts of your app interact with each other as expected.

How tests?

I will try out the different kind of tests inside the code of this getting started code lab for flutter, if you are interested in copying my tests.

https://codelabs.developers.google.com/codelabs/flutter-codelab-first#0

But these tests will be easy to understand and small enough to apply them to your own code.

Testing with unit tests

Remember, with unit tests we want to test small parts of our code. These tests will be the easiest to understand, in my opinion

first of all we need to add the test dependency to the project. Paste this in your projects terminal window.

$ flutter pub add dev:test

In your pubspec.yaml file under “dev_dependencies” you should now see the test dependency

dev_dependencies:
  flutter_test: // here
    sdk: flutter

  flutter_lints: ^2.0.0
  test: ^1.25.7

Now we create 2 files

app/
  lib/
    counter.dart
  test/
    counter_test.dart

Test files should always end with _test.dart, this is the convention used by the test runner when searching for tests.

We wan’t to create a test, that tests a member function of a class. The class has just one property of type int, with one function to increment the property by one and another to decrease it by one. And because we are using TDD, lets start with the test.

import 'package:fluttercodelab/counter.dart';
import 'package:test/test.dart';

void main() {
  group('Test start, increment, decrement', () {
    test('value should start at 0', () { // first test
      expect(Counter().value, 0); // check value is 0
    });

    test('value should be incremented', () { // second test 
      final counter = Counter();

      counter.increment();

      expect(counter.value, 1); // check value is 1
    });

    test('value should be decremented', () { // 3rd test
      final counter = Counter();

      counter.decrement();

      expect(counter.value, -1); // check value is -1
    });
  });
}

The test will currently not work, because we have not yet implemented the Counder class.

class Counter {
  int value = 0;

  void increment() => value;

  void decrement() => value;
}

We have 3 tests grouped together in a test group. The first test checks if the value ist 0, when initializing the class. The second test checks if the value is 1 after using the increment member function. The 3rd test checks if the value ist -1 after using the decrement member function.

We can run the test like this:

  • IntelliJ

    1. Open the counter_test.dart file

    2. Go to Run > Run 'tests in counter_test.dart'. You can also press the appropriate keyboard shortcut for your platform.

  • VSCode

    1. Open the counter_test.dart file

    2. Go to Run > Start Debugging. You can also press the appropriate keyboard shortcut for your platform.

  • Terminal

      flutter test test/counter_test.dart //all tests in the file
      flutter test --plain-name "Test start, increment, decrement" // only the test
      // with this name
    

If we run the test we get a TestFailure Exception

Exception has occurred.
TestFailure (Expected: <1>
  Actual: <0>
)

That’s great, we want the test to fail at the start, so we can correct it. The problem ist that in our member functions we just return the value instead of doing anything with it before. Lets correct that.

class Counter {
  int value = 0;

  void increment() => value++;

  void decrement() => value--;
}
// 00:04 +3: All tests passed!

Great, now everything works.

Widget testing

Widget testing is also pretty easy. The test code just creates the widget, that you want to test, and checks if the ui actually displays what you are expecting.

  1. Add the flutter_test dependency
$ flutter pub add dev:test
  1. Create a widget to test

Thats a widget from the code lab. Its mission is to display a word pair on a card like structure. A word pair is just a string, that is made up of 2 other strings added together.

class BigCard extends StatelessWidget {
  BigCard({
    super.key,
    required this.pair,
  });

  final WordPair pair;

  @override
  Widget build(BuildContext context) {
    var theme = Theme.of(context);
    var style = theme.textTheme.displaySmall!
        .copyWith(color: theme.colorScheme.onPrimary);
    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Text(
          pair.asLowerCase,
          style: style,
          semanticsLabel: pair.asPascalCase,
          textDirection: TextDirection.ltr,
        ),
      ),
    );
  }
}
  1. Create the test
import 'package:flutter_test/flutter_test.dart';
void main() {
  testWidgets('The BigCard Widget Shows a Wordpair', (tester) async{

  }); 
}
  1. Build the widget inside the test
import 'package:flutter_test/flutter_test.dart';
import 'package:fluttercodelab/main.dart';
import 'package:english_words/english_words.dart';
void main() {
  testWidgets('The BigCard Widget Shows a Wordpair', (tester) async{
    var pair = WordPair.random();
    await tester.pumpWidget(BigCard(pair: pair)); // creating the widget
  }); 
}

Important note from the official docs.

After the initial call to pumpWidget(), the WidgetTester provides additional ways to rebuild the same widget. This is useful if you're working with a StatefulWidget or animations.

For example, tapping a button calls setState(), but Flutter won't automatically rebuild your widget in the test environment. Use one of the following methods to ask Flutter to rebuild the widget.

tester.pump(Duration duration)Schedules a frame and triggers a rebuild of the widget. If a Duration is specified, it advances the clock by that amount and schedules a frame. It does not schedule multiple frames even if the duration is longer than a single frame.

infoNote To kick off the animation, you need to call pump() once (with no duration specified) to start the ticker. Without it, the animation does not start.

tester.pumpAndSettle()Repeatedly calls pump() with the given duration until there are no longer any frames scheduled. This, essentially, waits for all animations to complete.

These methods provide fine-grained control over the build lifecycle, which is particularly useful while testing.

  1. Find the desired output
void main() {
  testWidgets('The BigCard Widget Shows a Wordpair', (tester) async{
    var pair = WordPair.random();
    await tester.pumpWidget(BigCard(pair: pair));

    final pairFinder = find.text(pair.asLowerCase); 
    // find "pair.asLowerCase" inside the widget
  }); 
}
  1. Verify the widget with a Matcher
void main() {
  testWidgets('The BigCard Widget Shows a Wordpair', (tester) async{
    var pair = WordPair.random();
    await tester.pumpWidget(BigCard(pair: pair));

    final pairFinder = find.text(pair.asLowerCase);

    expect(pairFinder, findsOneWidget);
    // check if exaclty one instance was found.
  }); 
}

In addition to findsOneWidget, flutter_test provides additional matchers for common cases.

  • findsNothingVerifies that no widgets are found.

  • findsWidgetsVerifies that one or more widgets are found

  • .findsNWidgetsVerifies that a specific number of widgets are found.

  • matchesGoldenFileVerifies that a widget's rendering matches a particular bitmap image ("golden file" testing).

Thats it, the test passes. Nice!

Integration Test

Integration Tests can be a bit more challenging, because your are not testing something in an closed environment, but rather your whole app. When you do an integration test, a debug instance of your app will start and depending on what you have specified, the test actually performs actions like clicking on your app and ensures that it behaves as expected.

  1. Add dependencie in pubspec.yaml
dev_dependencies:
  flutter_test:
    sdk: flutter
  integration_test: // this one
    sdk: flutter // this one
  1. Create “integration_test” Folder in your Root Directory

  2. Create file “app_test.dart” inside that folder

  3. Import Statements

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

notice how we call the “main.dart” file “app”

  1. Write The test

Inside the app, which we are testing with this integration test, you can select wordpairs to be your favorite, by clicking the favorite button. Once thats done, the button changes from an outlined button (with border), to an filled one. The test should check if that is really happening.

look at the comments in the code to understand what is going on

void main(){
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('highlight wordpair', (WidgetTester tester) async{
    app.main(); //start the main function of your app (starting the app)
    await tester.pumpAndSettle(); //wait till the app is ready

    Finder button = find.byIcon(Icons.favorite_border); //find the button by its icon 
    await tester.tap(button); // click the button to test if highlighting works
    await tester.pumpAndSettle(); //wait till the app is ready

    expect(find.byIcon(Icons.favorite), findsOneWidget);
    // expected to find exactly one instance of the filled favorite icon 
  });
}
  1. Run Test in Terminal
flutter test integration_test // test passes

Conclusion

Tests are an important and powerful tool in software development and learning how to implement them is very valuable. I hope with this article i was able to show you how easy it is to implement simple tests. The hard part about testing is to maintain an rewrite your tests when your application grows. That is a challenge that i have yet to master and maybe in the future i will make a more detailed article covering that topic.

If you enjoyed reading this blog post, i would appreciate a like and feel free to comment any type of question or critique regarding my article.

Thank you

0
Subscribe to my newsletter

Read articles from Robin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Robin
Robin