Flutter TDD ๐Ÿงซ ๐Ÿงช ๐ŸŒก -  Testing your flutter app for good ๐Ÿ” ๐Ÿฆพ  (  Basics unit / widget tests ) Part 1

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

project flow

How we will approach our testing from the above map

We start in this order

  1. App prototype / mock

  2. write failing test to capture concept

  3. Design the app following the prototype and capture the failing scenarios

  4. 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

testing.drawio.png

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

Screen Shot 2021-10-25 at 11.57.22 AM.png

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.