Widget Testing - The Untold Story

An exploration of how the testing trivia can be carried out in Flutter projects during the development phase

cover_image

If you do not wish to test your Application, your customers also ain't gonna do so!

I strongly believe most people will accept the fact that testing your application is a prominent and integral part of your application building. Since our primary goal is to produce a robust and perfect product, testing becomes a key part before the product gets delivered to make it ideal.

Before diving deep into Flutter testing, I would like to discuss so general things about Development phase testing. I know most folks in our development community name the development phase testing unanimously as unit testing, but development phase testing not only succumbs to unit-level testing but has a lot more to do than that. Hence, I think naming it as 'Development Phase Testing' will give it the justice it deserves!

General Trivia on Testing

Most people in our developer community have this particular question rolling inside their heads.

I'm a busy Developer. How can I/Why should I?

Being a part of the community, I too ask the same question, but more than that, I think the best thing to do is to perform testing in the initial stages because Developing a product is an Art. It requires a lot of patience and strength to deliver the robust and ideal product as is expected. Hence, I strongly believe that it is a great practice to jot down test cases in the initial stages itself. By then you can ensure all your building processes are going fine step by step.

Flutter Testing: The Army

Now it’s time to get Fluttered. As everyone knows, Flutter has proven its power of building high quality, highly feasible UI apps and now has been growing as one of the giants in its own market. Testing Flutter apps is also one of the processes to build the sculpture properly from the preamble. To consummate the same, we have the following methods:

  • Unit Testing - The Soldier
  • Widget Testing - The Colonel
  • Integration Testing - The Field Marshal

Unit Testing - Testing the bits and pieces of everything in the project. Here you will be testing every unit in the code like variables, methods etc. 

Widget Testing - This is very special to Flutter. As you know, Flutter is made up of widgets. This helps us test the actions, reactions and functionalities of the widgets used.

Integration  Testing - Integration testing refers to testing a process or flow. Here, we dive deep into automating the entire action play of the app features in all possible scopes available. There are some special packages similar to automation testing tools like selenium,appium etc.

Testing Rulesets

Know the rules better, so you can break them effectively - Dalai Lama

There are some general rulesets that you have to follow while writing the test cases. This gives you a better foundation, improved readability and clear structures. Those rules are listed below:

  • Folder Structures - The Family Tree
  • Naming Conventions - The Core Members

Folder Structures - All the files in the library folder for which the testing is to be performed is to be added with the same structure inside the test folder. Since all our test files are written inside a separate directory called test, which is also a part of the Flutter project directory, we need to follow the same structure we do inside the lib folder.

Naming Conventions - Naming the test files well is one of the key parts of the process. Every test file you are naming must be suffixed with the word '_test' in order to let Flutter test package identify the test file and run the same and it also helps us to differentiate between the different files on its nature, like models, services or whatsoever it may be. After naming the file in the same method, you are able to run the entire test suite by running the command flutter test from the root level of your project, which identifies and runs all the files ending with the phrase '_test.dart'.

Packages

Now comes the helper hand - packages for testing. These packages help us to test each prominent features we are integrating with the Flutter application based on its functionalities:

  • test - The Common Man
  • flutter_test - The Framework Man
  • flutter_driver - The Automation Man
  • bloc_test - The Special Man

test - This is a package used for testing native dart related components like methods and other components. This can be fetched from https://pub.dev/packages/test

flutter_test - This is an inbuilt library that comes with Flutter SDK. You can test all the specific components related to Flutter and it helps us to create an entire test suite that corresponds to test Flutter features. There is some example code for the flutter_test package inside the example folder of Flutter official GitHub repository. One such can be referred here - https://github.com/flutter/flutter/blob/master/examples/hello_world/test/hello_test.dart

flutter_driver - Automation testing can be assisted through this package. This resembles the market pioneers like Selenium or Appium and works in the same way to test the Flutter application. This comes in play after the entire application is finished and when you have to create an automated suite that tests the permanent features after each release.

bloc_test - This is a special test package that helps us to test Bloc (the highly standard State Management technique). When a project is conceived with the Bloc State Management system, we can use this package to test the states emitted, order of states and the lot. The package link is https://pub.dev/packages/bloc_test

Implementation: The Live Show

Without further delay, we can get into the implementation of writing test cases for a working Flutter application.

For the demonstration, I have created a sample application which is a fancy version of the counter application which is popular among the Flutter dev community. A small demo of the app functionality is below:

ezgif.com-gif-maker.gif

In the application, we have three simple components - one text widget with the string "It`s Always success" and an IconButton of heart shape which increments the number of likes received and shows it using the number of likes displayed. We use the setState({}) method to show the changes in the local state while clicking on the button and incrementing the number of hearts received.

Code Structure

  • Since we need to write multiple test cases, we use the group method given by the package. It takes two parameters - a string for the description of test cases and an anonymous function where we jot down the test cases.

Screenshot 2020-10-05 at 10.38.18 AM.png

Sample structure for group method

Main method

  • For any action to happen in our code, we need a main method to execute. Hence, the test script also expects the main method from where all the actions kickstart. Here, we need to note one thing that, for any test script to run, we need a virtual UI to ratify the test execution. Hence inside the main method, we create a method makeTestableWidget. This method takes a widget as a parameter and renders it inside the MaterialApp widget which is embedded inside the method. Hence the child widget is received as a parameter and sent inside the MaterialApp to create a virtual UI, all happening at the backside. Our test script will run over the virtual UI and produce the results based on the action executed.

Screenshot 2020-10-05 at 10.46.13 AM.png

Main method with makeTestableWidget embedding

Import Statements 

  • Also, it is important to import all the packages and files where we need to execute the testing - Here we are importing the flutter_test package for performing our testing, material library from dart to execute the virtual UI, home_screen.dart file to fetch the widgets and pass it to the makeTestableWidget method inside main method as child parameters and keys.dart file where we have declared Keys used to reference the widgets inside the application.

Screenshot 2020-10-05 at 11.03.05 AM.png

All import statements required for test script

Test Case -1: Finds One Title Widget: Positive Scenario

In this test case, we are going to check whether the title widget is present are not. For this, we need to use the testWidgets method. This method helps us to perform the Widget test to see if the title widget("Its Always Success" - Text widget) is present or not. Contemporarily, the testWidgets method also takes two parameters - one description string and an anonymous method inside where we write the test case execution scripts. This anonymous method takes up a parameter object - tester of class WidgetTester. This is the key point of the test script which helps us to take the reference of test methods and execute all actions like drawing the virtual UI frames.

We will go through the steps one by one and see how a test case execution code is been sculptured:

  1. At first, we need to check the title Text widget which is internally a string. Hence we take up the reference of the title caption in a local variable String title = 'It`s Always Success';

  2. Now with the help of the tester, we call a method called pumpWidgetThis helps us to render the virtual UI and can be considered as an alternate build method that we use quite a lot inside our Flutter development code. Hence we call the makeTestableWidget method inside the main method from here and pass the HomeScreen widget which we want to render virtually as a child. Hence, the method invocation goes to main method and embeds the HomeScreen Widget inside the MaterialApp. By then we have the virtual UI ready to execute the test. - await tester.pumpWidget(makeTestableWidget(child: HomeScreen()));

  3. Now we have the UI ready and the text to be found. Its time to find the title Text widget using "find.text(title)". As the name implies, we are fining the title Text widget and storing the reference inside the variable titleFinder and find.text() method helps in finding the reference using the passed text. In a similar way we have find.byKey(), find.byWidget() etc. to get the reference of the corresponding widgets. Now, if there is a widget with the sent title, you will store the reference in the titleFinder variable. Even if there is no widget, you ll never get any exception - final titleFinder = find.text(title);

  4. Generally, testing is expecting. Hence now we use a method expect()which takes two parameters, the actual result on the left side and the expected result on the right side. Hence now we have the Text widget reference titleFinder on the left-hand side and findsOneWidget method on the right side. What happens apparently is the findsOneWidget method checks for one widget with the reference titleFinder passed on the left side. Hence if the reference is there, we will get the test script result as passed, else the script fails. This marks the end of the test case which can be seen at the IDE run panel or in the terminal if it runs inside there. - expect(titleFinder, findsOneWidget);

Screenshot 2020-10-05 at 12.05.59 PM.png

Test Case -1: Finds One Title Widget - Positive Scenario

Screenshot 2020-10-05 at 12.17.05 PM.png

Test Case 1 - Passed for Positive Scenario

Test Case -2: Finds One Title Widget - Negative Scenario

It's evident that for a Testing engineer, covering the negative scenarios is just as important as covering up the positive scenarios. Here in this test case, I will follow all the steps described for Test Case 1, except for the expect method that we are trying to achieve. The ideology behind this test case is that now we have a widget Title which has the String "It`s Always Success" and we have proven it in the previous test case. What if we try to deceive the tester component by saying there is no such widget - expecting findsNothing and passing the titleFinder. Now actually the findsNothing method will expect nothing of such reference passed to the left side (titleFinder) but the widget is already there. Hence you will see the test case failing and this is also a prominent confirmation that your app does not act out of box as expected.

Screenshot 2020-10-05 at 12.55.01 PM.png

Test Case -2: Finds One Title Widget - Negative Scenario

Screenshot 2020-10-05 at 12.56.55 PM.png

Test Case 2 - Failed for Negative Scenario

Test Case -3: Testing the Icon Button Using Key - Positive Scenario


This test case defines finding the button available (The Heart IconButton Widget). Here the difference is to refer the button using a Key - the boon of Flutter. Using a key, we can refer the widgets on a global scale and hence we will see how to get the widgets referred to using a key inside test scripts.

First of all, I have defined a separate file keys.dart where I have defined the string values for all the keys I'm using to refer the widget as follows:

Screenshot 2020-10-05 at 1.01.45 PM.png

Keys.dart used to define all the keys implied inside the application

I have made the strings static so that I`m able to refer them globally.

Now inside the test script, I'm following the same steps described for the previous test cases. The change happens only at the place where we find the widget reference and here I`m using find.byKey() method to get the widget reference using the key. I define a variable buttonKey where I create a Key object by sending the static reference of the string we created- final buttonKey = Keys(Keys.icon_button);. Now I'm passing the buttonKey variable inside the find.byKey method to get the expected reference of the heat IconButton in the HomeScreen. final buttonFinder = find.byKey(buttonKey); and finally we use expect method and findsOneWidget to see whether one widget is present in the HomeScreen and the test case passes.

Screenshot 2020-10-05 at 1.18.51 PM.png

Test Case -3: Testing the Icon Button Using Key - Positive Scenario

Screenshot 2020-10-05 at 1.19.27 PM.png

Test Case 3 - Passed with Positive Scenario

Test Case - 4: Testing the Icon Button Using Key - Negative Scenario

As illuminated previously, we try to cover up a negative scenario for the previous case by expecting nothing using expectsNothing widget for the widget which is already landed.

Screenshot 2020-10-05 at 1.28.00 PM.png

Test Case -4: Testing the Icon Button Using Key - Negative Scenario

Screenshot 2020-10-05 at 1.29.06 PM.png

Test Case 4 - Failing for Negative Scenario

Test Case -5: Check Button Actions and find whether hearts are incremented - Positive Scenario

Its time to get into some action. Here, let's look into some of the actions taken on the HomeScreen and test whether the re-actions are proper. Putting it in a statement, we are going to click on the heart Icon button and see whether the hearts are incremented and the number widget adds up one on each click.

  1. At first, we will take the button reference to make click action on it. I will use the same Key methodology to get the reference of the heart button and save it in the variable buttonFinder.

  2. Now before any action happens, the number text will be '0'. Hence for taking reference of the same, we use the find.text("0") method and store its reference in the variable heartsFinder.

  3. For the action to happen now, we have a method called tap which comes along with the tester object. This method helps us to make a tap action on the reference passed. Hence we pass the reference of the buttonFinder inside the tap method to make a button action - await tester.tap(buttonFinder);

  4. After the tap action for the button has happened, it's mandatory to re-render the widget, which we do in the real-time application using setState({}) to re-render the local state changes. Analogously, for the virtual UI we created to the test scripts, we have a method called pump which helps to rebuild the HomeScreen widget. Hence we call the pump method using tester object reference and re-render the UI, where now the number widget will have been transformed to "1" - await tester.pump();

  5. Now for checking the change of the number, we create a new reference variable called heartsFinderAfterTapping. Here, we find the text widget with the string "1". Using the findsOneWidget method, now we confirm that there is one widget holding text "1" with the help of expect method and passing heartsFinderAfterTapping reference.

  6. You can cover up an additional case, by passing the older reference heartsFinder(reference for "0") the method findsNothing to re-confirm no older widget exists now and the UI has changed after calling the pump method. You will be excited to see now all the two scenarios are passed now.

Screenshot 2020-10-05 at 2.39.35 PM.png

Test Case -5: Check Button Actions and find whether hearts are incremented - Positive Scenario

Screenshot 2020-10-05 at 2.41.02 PM.png

Test Case 5 - Passed for Positive Scenario

Group Test Running:

We can run the group method on top level to get the consolidated results of all the test cases described inside. In our case, 3 cases have passed (Positive scenarios) and 2 cases have failed(Negative scenarios), which is expected.

Screenshot 2020-10-05 at 2.45.20 PM.png

Group Test Running showing 3 Passed and 2 Failed test cases as expected

Running tests from terminal:

You can use the command flutter test from the project root level to run all the files in your test folder. This command will identify the files suffixed with "_test.dart" and run the test cases enriched inside the files. The terminal result for our test script will be as follows:

Screenshot 2020-10-05 at 2.58.21 PM.png

Terminal result showing 3 passed and 2 failed cases

An ideal test suite must give hope to the developers that they can rely on results. Hence, it's very important to carefully write the test cases and make sure it gives hope to make the application a robust one. In that way, development phase test script writing becomes one of the important features, not only for a Flutter project but for any application developed in any coding language.

The link for GitHub Repository for the complete Flutter project along with the test script is available in the testing branch here - https://github.com/devanandp/Scribblings/tree/testing.

Star the repo and give us a clap to invigorate us contributing to the technology world. Sending the readers a Thanks and 👋 until we meet next time!

2020 © All rights reserved. GeekyAnts India Pvt Ltd.