How to use test-driven development (TDD) with Flutter
Instructions how to use test-driven development (TDD) with Flutter from ArcTouch's cross-platform app engineering team.
5 min. read - November 12, 2020
Although it’s relatively new, Flutter is already one of our top cross-platform app development tools. And QA testing is one of our favorite app development topics. So, let’s combine these. Specifically, what’s it like using test-driven development (TDD) with Flutter? This post provides step by step instructions on how to use TDD to QA test a basic note-taking app built in Flutter.
But first…
What is test-driven development?
Test-driven development is the practice of writing QA tests for each unit of the app before any code is implemented. At the start of development, since there’s no app written yet, each test fails. Each development cycle is then run with the idea of writing the minimum code needed to make a test pass. Then the code is analyzed and refactored if needed. Then the cycle starts again.
There are a lot of benefits to using TDD:
Improves code quality: TDD helps focus thinking about what needs to be done first.
Well-written tests serve as documentation: Developers read the tests to understand expected functionality.
Gives you more confidence when changing code: After a test has started passing you know quickly if refactoring has broken something when it starts failing.
Helps reproduce bugs: TDD helps resolve bugs. When a bug fix is needed, you first write a test that fails because of the bug. And when the test passes, you know it’s fixed.
Flutter testing
Flutter includes a testing framework that makes using TDD extremely easy. Flutter’s testing documentation provides an overview of the types of tests you can write:
A unit test of a single function, method, or class.
A widget test for a single widget.
An integration test for a complete app or a large part of an app.
Ideally, an app should have enough unit tests to cover the business logic, widget tests to cover how UI reacts to business logic state changes, and integration tests to cover higher-level use cases.
Using Test-Driven Development with Flutter
To show how to use Flutter and TDD, let’s build a simple note-taking app.
Our app user requirements are:
Users can see a list of the notes.
Users can create a note.
Users can edit a note.
Users can delete a note.
We won’t spend a lot of effort on the UI. Instead we will show step by step instructions to demonstrate the standard TDD cycle: test fails, test passes, refactor.
A link to the GitHub repo with the fully working example is at the bottom of this post. You can browse the commits to see the process with the refactorings.
Start by creating a new project. Use the “Flutter Create” feature or create the project through any IDE.
For this application, we use cubit to manage states and equatable as our equality methods. So, make sure to add flutter_bloc:^6.0.5
and equatable: ^1.2.5
dependencies to your pubspec.yml file.
Step 1: Write the first TDD tests using Flutter cubit
The core of our app is our Flutter cubit, The cubit needs a note model and its states. Thinking about our app requirements, we have a simple list of notes as our state, and use methods to create, edit, and delete notes — which is basically a CRUD. We could write one test at a time, but to keep these instructions simpler, let’s add all of then in one shot:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
The test implementation is pretty straightforward: We create our cubit, call the method, and make sure that the state is as we would expect. Some people would write the interface before the test. That’s OK, but for this post, we’re going to do the test first.
Note that we are testing only the happy path of this class. Later on, we add tests for what happens if the user deviates from the happy path, such as trying to delete a note that doesn’t exist.
Step 2: Implement the cubit
Now that we have our tests, we implement the cubit to make them pass. We need to write a Note
model class that has an ID, title, and body. And for our NotesCubit
, we have a state with a list of notes and three methods to change the states. To change the state of a cubit, we call the emit method. So for each of our methods, we process our notes list and emit a new state with its contents:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
If we run our tests again, all of them should pass. Now we can do some refactoring. You can improve the cubit code and move each class to its own file.
Step 3: Start working on the UI
Once the business logic is working and passing the tests, we move to the UI of the app. In Flutter, we can test widgets, so we continue our TDD process of writing tests first, and then implementing the code.
To test a widget, we provide the full context for its life cycle. Otherwise, our widgets wouldn’t be able to do things like receive events or perform layout. Flutter’s testing framework provides pretty much everything we need to set up a context and set up unit testing.
The page UI is a list of notes, so we implement this using a simple ListView
and a ListTile
to represent each note. We use the find.byType
method to find our widgets in the test. This method will look within our widget tree for any widget that matches the type we defined. We can also take advantage of the find.text
method to make sure our UI is displaying the note information correctly. As our notes are handled by our NotesCubit
, we inject this dependency in our HomePage
constructor:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Our tests are failing, as expected. And that’s a good thing.
Step 4: Create the app home page
Next we start developing the home page. We can either change the one that comes with Flutter’s empty project or create a new file and update our MaterialApp
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
The tests are now passing — great! If you didn’t create this page in its own file, it would be a good time to refactor and run the tests again. We can also remove everything from the default empty project that we won’t use.
Step 5: Create a new page
To add new notes, we need a new page. Let’s call it NotePage
. It will be accessed by tapping the floating action button that comes with Flutter’s empty project:
1
2
3
4
5
6
7
8
9
10
11
For this test to pass, we have to implement navigation to a new NotePage
. This page also receives our cubit in the constructor:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Step 6: Add a test for note creation
The NotePage handles creating, editing and deleting notes. The first thing we are going to support is note creation. The UI is as simple as possible: two TextField
widgets for title and body respectively, and a button to confirm the action. After the user confirms, we close the page. We can write two tests for this: Assert the initial state of the page, and actually create a note. Our initial state is going to be two text fields with a hint text. The button should be disabled:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Note the ValueKey
in the text fields — with this, we can easily insert values as needed.
Step 7: Write the NotePage UI
With those tests written, we have a better understanding of how the page should work, including the special behavior for the confirmation button. Now write the complete NotePage
UI:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
At this point, we have a simple note-taking app — not the most beautiful one, but it works. Still, we won’t run the full app just yet. We have two more steps to complete.
Step 8: Create tests for HomePage and NotePage
For the note creation feature, we should add tests for both HomePage
and NotePage
. These ensure that if the user taps on a list item, we send our note to the next page so it can be edited. For the NotePage
, the tests are going to be pretty similar to what we already have. We assert the initial state when we receive a note as a parameter. In that case, both text fields should be pre-filled with the note title and body. The second test is to assert the note editing, similar to note creation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
We refactor the NotePage
to receive a Note
in its constructor so it will show up in edit mode. We set the text of our controllers in the initState
method if our note is not null
. And when the user confirms by tapping the button, we call the updateNote
instead of the createNote
. We add the ListTile
tap action in our HomePage
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
Step 9: Implement the delete note feature
With a small change, we can implement the delete note feature. On our NotePage
, we add a delete icon to our AppBar
. This icon should be disabled if we are creating a note. For simplicity, we are not going to ask for confirmation — just delete the note and go back to the home screen. Some tests have to be updated, so we assert that the icon is disabled if we are not actually editing a note. Here’s the full note_page_test.dart
snippet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
And here’s the change in the NotePage
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
And that is it!
Run the app and try it out. It should be working as expected because all the TDD tests have passed.
Your turn: Try using TDD with Flutter now
This isn’t the prettiest app in the world. But it helps you understand a little bit more about a test-first philosophy and how to use Flutter with test-driven development. Here is the full example in the GitHub repository. In the commits, you will see how the app was built, pretty much following the same step-by-step instructions in this post, plus the refactoring. There are no refactoring snippets because it is hard to properly show the differences.
Need Flutter app development help?
Since the dawn of the App Store, ArcTouch has helped companies build lovable apps and digital products. Our cross-platform app development team are experts in Xamarin, Flutter, and React. Contact us, and let’s build something lovable together.
Article Author:
Subscribe for more insights
Get our newsletter in your inbox.
Contact us.
Let's build something lovable. Together.
We help companies of all sizes build lovable apps, websites, and connected experiences.