Flutter TDD ๐งซ ๐งช ๐ก - Testing your flutter app for good ๐ ๐ฆพ ( Basics unit / widget tests ) Part 1
We will achieve a basic understanding of the importance of testing your flutter app, this will form the overall importance of testing in general
Hey friend, thanks for stopping by, this is actually my first blog post here, and I am excited to share with you, something i think will be very useful in your flutter dev journey.
Pardon me if i don't go so deep, this is meant for beginner testers in flutter or advanced developer in flutter but have not been testing your app.
Here we are going to build a basic mobile app screen which can serve as a part of a big puzzle in your app flow.
So my assumption is that you have built app on flutter before, at least you have flutter installed and IDE like VS Code or Android studio; if you have that already let's move ๐๐พ on!
What we will achieve here is the same strategy you can apply in other project with a little incremental in the quantity of tests and screens your app has.
This tutorial is breaking into parts.
Part one we will handle the
- Tests for both Login, Signup and in the second part we will
Part two we will
- Create the 3 screen as shown below and run our final test to pass.
Test Mind map
How we will approach our testing from the above map
We start in this order
App prototype / mock
write failing test to capture concept
Design the app following the prototype and capture the failing scenarios
run test until its meets all scenarios and passes
So we are dive to the first step,
Step one ( App prototype or mock)
(The design below is done brevity in draw.io app) but can be fleshed out using Figma, i used it to save time;
I am lazy today!. We can have a fresh post on Figma later, (leave a comment if you want that)
So in this tutorial we are going to build this screens below in step step 3 of this flow
Without wasting more time let's do fast and get to the code and write test cases to handle the following scenario of our app.
Step Two ( Failing test)
Our app will be listing a flute music instruments, we want to be able to make sure for the 3 screens that we captured all needed scenarios and features before we can consider it a pass, in your personal project; this depends on the app and how tight or loose you make your test, here we are doing a basic house keeping test which forms the foundation and give you a core clue and need for testing and the problems it takes off your back when you make it a life style in any app project. Lets get to it ๐๐ฝโโ๏ธ
Login screen
For the login screen we want to:
Find the heading text with the exact text label "flute Login" as seen in the Login image above when the app is launched and also
Find the 2 inputs fields for username and password and finally
Find the login button on our screen
So when we write this test and run it, the expectation is that it will fail, which is not bad since we have not created the screen yet.
So after writing all the tests and we captured all these scenarios we can go ahead to create the UI. this type for test in flutter is called widget testing since we are testing the UI component not the logic; We will go ahead and run the test again until it passes.
So Let's go to the code. I am using android studio for this tutorial but; VS code is awesome if thats what you have.
Create a new Flutter project and give it any name; I called mine flutested
Once you have created your app head to the test folder
Create a new test file in that folder and call it flute_login_test.dart
remember the name must end with _test.dart
for flutter to be ale to capture it as test
Go ahead and run the app just to make sure all things are intact; if your app runs with the default counter app? great ๐ค.
๐ Head back to the test folder and open the flute_login_test.dart
file and add the following code.
//import all necessary file
void main() {
// we use function this to set up our root app widget
Future<void> _buildMainPage(WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: LoginPage(title: "Login Page"),
),
);
}
group("Login Page", () {
testWidgets('Has a title of `Flute login`', (WidgetTester tester) async {
// Build our app and trigger a frame.
await _buildMainPage(tester);
// ... other testing goes here
});
});
}
Let explain what we have in the code so far. Since we are doing a widget test, the widgets we want to test are the text and input as seen in the login screen;
First we create a private function _buildMainPage(WidgetTester tester)
to setup our login screen where we hook up other widget; we called the pumpWidget()
method from the WidgetTester object to setup the widget in question. in the code above we passed our app: FluteApp
; which is a widget actually to the method. with that our app screen is setup, we can now attach other things for testing.
With a widget to test, Inside the main method, which wraps all test functions; We grouped the possible related tests, as we have in the login page; we use the testWidgets()
function provided by the flutter_test
package inside the group we just added to define individual test.
The testWidgets
function allows you to define a widget test and creates a WidgetTester
to work with. This function takes two argument the description of the test and WidgetTester
instance which we will use afterwards to setup things like binding our App or other widget events.
Lets add more code to the test and run our first test; So just below the comment, // ... other test add the following lines
expect(find.text("Flute Login"), findsOneWidget);
Ok. enough of the code lets check out what we have done so far. ๐ ๐บ
Let's run the test. ๐ฃ๐ฝ head to the project terminal and run this command.
flutter test test/flute_login_test.dart
Remember to type in the name you used for the test, just in case its not the same with mine.
We expect this test to fail because we have not built the parent UI components
So this is the error ๐ก I got.
00:03 +0 -1: Login Page Has a title of `Flute login` [E]
Test failed. See exception logs above.
The test description was: Has a title of `Flute login`
00:03 +0 -1: Some tests failed.
So far you are doing well ๐ฅท ๐ฅท
Lets add the remaining test cases for the login screen
So directly below the first test inside the group
function add these 2 test for inputs and button
testWidgets('Has 2 `input form username and password`', (WidgetTester tester) async {
// Build our app and trigger a frame.
await _buildMainPage(tester);
expect(find.byType(TextInput), findsNWidgets(2));
});
testWidgets('Has a ` a login button`', (WidgetTester tester) async {
// Build our app and trigger a frame.
await _buildMainPage(tester);
expect(find.byType(ElevatedButton),findsOneWidget);
});
wooow ๐ฅฐ we did it.
The complete login page test looks like below, remember to import missing dependencies
void main() {
Future<void> _buildMainPage(WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: LoginPage(title: "Login Page"),
),
);
}
group("Login Page", () {
testWidgets('Has a title of `Flute login`', (WidgetTester tester) async {
// Build our app and trigger a frame.
await _buildMainPage(tester);
expect(find.text("Flute Login"), findsOneWidget);
await tester.pump();
});
testWidgets('Has 2 `input form username and password`', (WidgetTester tester) async {
// Build our app and trigger a frame.
await _buildMainPage(tester);
expect(find.byType(TextInput), findsNWidgets(2));
await tester.pump();
});
testWidgets('Has a ` a login button`', (WidgetTester tester) async {
// Build our app and trigger a frame.
await _buildMainPage(tester);
expect(find.byType(ElevatedButton), findsOneWidget);
await tester.pump();
});
});
}
So far so good, we have made some progress, It will make sense to explain what some of these codes are doing; So let's tear apart what the extra lines of code is doing.
We have three test cases inside the group test
First we checked to find if the heading text "Flute Login" is present in the screen
Secondly, we checked to confirm, if we do have 2 text input widget for collecting username and password.
Thirdly we checked to make sure the login button of type elevated button is present in the screen
The Description of the test should aways be very clear to read when the test is run to clearly state what it will achieve..
Now we have some useful methods we use in testing scenarios, Methods like ๐คท๐ฝโโ๏ธ
expect()
find
we use the expect()
just like the name clearly spells out; what we are expecting from this test. In our case, for the first test; We expect to find the text "Flute Login" in the screen, So we pass that expectation as a description to the expect()
, and secondly pass a find
object telling it what we actually want to find: in our case, the text "Flute Login", and the last argument is a method that does the big magic, it returns our expectation in affirmation or opposite, by this I mean, for our example one.
We are expecting to find the text "Flute Login", so we passed findOneWidget
this method returns true if it found what we are looking for or false if it could not find it. We can as well make this test to pass simply by passing another method foundNothing
which will make our test to pass since we really don't have this text created yet in our app.
Also you will notice that the find
object has methods for finding a specific thing, for our first case we did a find.text()
which looks for exact text you passed, in the second one we did a find.byType()
its going to look for a widget of the type you passed to it eg. ElevatedButton and the list goes on, there are other useful find methods like find.byWidgetPredicate()
etc.
Lets ๐ ๐ head to the second test fast since we have explained what we are going to use there, we won't waste much time; just write our failing tests.