Getting Started with Test Writing in Flutter for Newbies
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
Open the
counter_test.dart
fileGo to Run > Run 'tests in counter_test.dart'. You can also press the appropriate keyboard shortcut for your platform.
VSCode
Open the
counter_test.dart
fileGo 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.
- Add the flutter_test dependency
$ flutter pub add dev:test
- 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,
),
),
);
}
}
- Create the test
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('The BigCard Widget Shows a Wordpair', (tester) async{
});
}
- 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()
, theWidgetTester
provides additional ways to rebuild the same widget. This is useful if you're working with aStatefulWidget
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 aDuration
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 callspump()
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.
- 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
});
}
- 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.
findsNothing
Verifies that no widgets are found.findsWidgets
Verifies that one or more widgets are found.
findsNWidgets
Verifies that a specific number of widgets are found.matchesGoldenFile
Verifies 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.
- Add dependencie in pubspec.yaml
dev_dependencies:
flutter_test:
sdk: flutter
integration_test: // this one
sdk: flutter // this one
Create “integration_test” Folder in your Root Directory
Create file “app_test.dart” inside that folder
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”
- 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
});
}
- 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
Subscribe to my newsletter
Read articles from Robin directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by