Blog-Image
  • Codeaamy
  • March 5, 2024

  • 0 Comments

Widget and Integration Test

This is the Fourth article in the “Master Testing In Flutter” series. The Previous articles were about Unit Testing the Http function to get all the Todos from the API, testing the change notifiers.

If you have not read the previous blogs, I would advise you to read them before you continue with the Widget and Integration Test as this is a continuation of the previous blogs. Here is the list of articles in this series, in case you want to read it.

Introduction

In this session we will learn widget testing and how to Widget Test the widgets. The Main goal of widget testing is to test the UI of the application, if its rendering as intended. The main difference between a Widget Test and an Integration Test is that in Widget Test you are testing the UI of the application and in Integration Test you are testing the UI and the functionality of the application.

As I mentioned earlier Widget Tests runs in the Console, headlessly and Integration Tests runs on the device. So Widget Tests are faster than Integration Tests.

Let us start with Widget Testing. We will be writing a Widget Test for the Login Screen.

First Widget Test

Head over to the test folder and create a new file called as login_widget_test.dart . You will be writing your widget test code inside this file.

We always start with main function. So let us write a main function.

// inside login_widget_test.dart file
void main() {

}

In Unit Test we used to call test function to write the test. Similarly in Widget Test we will be calling a function named testWidgets. This function takes a string and a function as an argument. The string is the name of the test and the function is the test function. To call this function you need to import the flutter_test package.

import 'package:flutter_test/flutter_test.dart';

void main() {

  testWidgets('Login Widget Test', (WidgetTester tester) async {

  });
}

WidgetTester is a class which provides a way to test widgets. You can use this class to find widgets in the widget tree, read and write the state of the widget, and verify that the widget has certain properties.

Let start writing our first widget test. When we start widget the first thing we should is try to expect a widgets presence which is static and wont change. Here we have a text widget which says Login Screen. Let us try to find this widget and check if it is present in the widget tree.

To find a if a widget is rendered we must first render the widget. It is done using the pumpWidget function. This function takes a widget as an argument and renders the widget.

so inside the testWidgets function lets pump the login screen.


// inside testWidgets function
testWidgets('Login Widget Test', (WidgetTester tester) async {
  await tester.pumpWidget(LoginScreen());
  expect(find.text('Login Screen'), findsOneWidget);
});

Like in unit test we had expect function to check if the test is passing or failing. Similarly in widget test we have expect function to check if the widget is present in the widget tree. The expect function takes two arguments. The first argument is the widget which you want to find and the second argument is the matcher. The matcher is a function which checks if the widget is present in the widget tree.

here we are finding the Widget by text. So we are using find.text function. This function takes a string as an argument and returns a widget. If the widget is present in the widget tree it will return the widget else it will return null. and we are using findsOneWidget matcher which checks if the widget is present in the widget tree. If the widget is present it will pass the test else it will fail the test.

Now let us run the test and see if it passes.

The Test fails with an error _No MediaQuery widget ancestor found_ This is because we are trying to find a widget which is not present in the widget tree. So we need to add a MaterialApp widget as a parent to the LoginScreen widget.

 testWidgets('Login Widget Test', (WidgetTester tester) async {
    await tester.pumpWidget(
      ProviderScope(
        child: MaterialApp(
          home: LoginScreen(),
        ),
      ),
    );
    expect(find.text('Login Screen'), findsOneWidget);
  });

Run the test and it should pass now.

Congratulations you have successfuly written your first widget test.

Photo by Eilis Garvey on Unsplash

Finding Widget For Testing

Now that we have know how to test a text widget. Lets find out other ways to find a widget in the Widget tree.

There are many ways to find a widget in the widget tree. The most common way is to find a widget by its type. For example if you want to find a widget of type Text you can use find.byType function. This function takes a type as an argument and returns a widget. If the widget is present in the widget tree it will return the widget else it will return null. and we are using findsOneWidget matcher which checks if the widget is present in the widget tree. If the widget is present it will pass the test else it will fail the test.

testWidgets('Login Widget Test', (WidgetTester tester) async {
  await tester.pumpWidget(MaterialApp(home: LoginScreen()));
  expect(find.byType(Text), findsOneWidget);
});

The problem with this approach is that if you have multiple widgets of the same type in the widget tree, this function will return the first widget it finds. So if you want to find a specific widget you can use findsNWidgets matcher. This function takes a type and an integer as an argument. The integer is the number of widgets you want to find. If the number of widgets found is equal to the integer it will pass the test else it will fail the test.

Another way to find a widget is by using find.byKey function. This function takes a key as an argument and returns a widget. If the widget is present in the widget tree it will return the widget else it will return null. and we are using findsOneWidget matcher which checks if the widget is present in the widget tree. If the widget is present it will pass the test else it will fail the test.

testWidgets('Login Widget Test', (WidgetTester tester) async {
  await tester.pumpWidget(MaterialApp(home: LoginScreen()));
  expect(find.byKey(ValueKey('loginScreenTextKey')), findsOneWidget);
});

I personally prefer using find.byKey function because it is more reliable and it is easy to find a widget by its key.

If you head to key_contants.dart file you will find that we have created a key for each widget. So that we can find the widget by its key.

if not, you can just add the following code to key_constants.dart file.

//key_constants.dart file to have all the keys

const loginScreenTextKey = ValueKey('loginScreenTextKey');
const emailTextFormKey = ValueKey('emailTextFormKey');
const passwordTextFormKey = ValueKey('passwordTextFormKey');
const loginButtonKey = ValueKey('loginButtonKey');
const loginCircularProgressKey = ValueKey('loginCircularProgressKey');
const loginButtonTextKey = ValueKey('loginButtonTextKey');
const quotesCircularProgressKey = ValueKey('quotesCircularProgressKey');

const kEmailErrorText = 'Please enter a valid email';
const kPasswordErrorText = 'Password must be at least 6 characters';

PS: Do not forget to add the key constants to widget that you want to test

I have already passed the key values to the widgets in the login screen. So that we can find the widget by its key. Head to login_screen.dart file and you will find that we have passed the key values to the widgets.


// Login screen 
  Padding(
    padding: const EdgeInsets.all(10.0),
    child: Text(
      'Login Screen',
      key: loginScreenTextKey,
      style: TextStyle(color: Colors.black, fontSize: 20),
      ),
    ),

Now that we know how to find a widget in the widget tree. Lets find out how to pump a widget.

putting it simply, pumping a widget means building a widget. When you call pumpWidget function it builds the widget and returns a Future.

pumpAndSettle function is used to pump a widget and wait for all animations to finish. This function takes a widget tester and a duration as an argument. The duration is the time to wait for the animations to finish.

Lets check if all the widgets present in the login screen are rendered properly. Lets write test to check that, I will be using find.byKey function to find the widgets.

testWidgets('Login Widget Test', (WidgetTester tester) async {
  await tester.pumpWidget(MaterialApp(home: LoginScreen()));
  final loginText = find.byKey(loginScreenTextKey);
  expect(loginText, findsOneWidget);
});

Run this test to check if our widget is rendered properly and also to test if find.byKey function works properly.

and the test passes.

Photo by Olav Ahrens Røtne on Unsplash

Okay Here is a Cool Widget test challenge for you. I want you to write a test to check if the emailTextField, passwordTextField and loginButton are rendered properly.

I hope you were able to write the test, Here is the Solution

testWidgets('Login Widget Test', (WidgetTester tester) async {
  await tester.pumpWidget(MaterialApp(home: LoginScreen()));
  final loginText = find.byKey(loginScreenTextKey);
  final emailTextField = find.byKey(emailTextFormKey);
  final passwordTextField = find.byKey(passwordTextFormKey);
  final loginButton = find.byKey(loginButtonKey);

  expect(loginText, findsOneWidget);
  expect(emailTextField, findsOneWidget);
  expect(passwordTextField, findsOneWidget);
  expect(loginButton, findsOneWidget);

});

Testing Other Scenarios of Login Page

Now test lets test the login screen with different scenarios. Before start lets jot down the scenarios.

  • When user enters invalid credentials
  • Users enters enters valid credentials
  • Testing the Circular loading indicator

When user enters invalid credentials

Lets enter invalid email and password into the text form fields and tap the login button. Then we will check if the error text is displayed or not. If the error text is displayed it will pass the test else it will fail the test.

await tester.enterText(emailTextField, 'abcd');
  await tester.enterText(passwordTextField, '1234');

  await tester.tap(loginButton);

  await tester.pumpAndSettle();
  var emailErrorText = find.text(kEmailErrorText);
  var passwordErrorText = find.text(kPasswordErrorText);

  expect(emailErrorText, findsOneWidget);
  expect(passwordErrorText, findsOneWidget);

We use tester.enterText function to enter text into the text form fields. This function takes a text form field and a string as an argument. The string is the text you want to enter into the text form field.

Then we tap the login button using tester.tap function. This function takes a widget as an argument. The widget is the widget you want to tap.

Then we use tester.pumpAndSettle function to wait for the animation to finish. This function takes a duration as an argument. The duration is the time you want to wait for the animation to finish.

Then we use find.text function to find the error text. This function takes a string as an argument. The string is the text you want to find.

Then we use findsOneWidget matcher to check if the error text is present in the widget tree. If the error text is present it will pass the test else it will fail the test.

Users enters enters valid credentials

Now that you know how to test the login screen with invalid crendetials. . lets Test the login screen for valid credentials.

  await tester.enterText(emailTextField, 'abcd@mail.com');
  await tester.enterText(passwordTextField, 'abcd1234');

  await tester.tap(loginButton);
  await tester.pumpAndSettle();
  emailErrorText = find.text(kEmailErrorText);
  passwordErrorText = find.text(kPasswordErrorText);

  expect(emailErrorText, findsNothing);
  expect(passwordErrorText, findsNothing);

Now you know how to test the Widget tests works.

There is more Widget which is missing from the test. Can you guess what it is?

The Loading Indicator, yes thats missing from the test. Lets add the loading indicator to the test.

The loading indicator is shown when the user taps the login button. So we will tap the login button and check if the loading indicator is present in the widget tree. If the loading indicator is present it will pass the test else it will fail the test.

  await tester.enterText(emailTextField, 'abcd@mail.com');
  await tester.enterText(passwordTextField, 'abcd1234');

  await tester.tap(loginButton);
  await tester.pumpAndSettle();
  expect(find.byKey(loginCircularProgressKey), findsOneWidget);

Does the test pass or fails. It fails, why? Because as mentioned previously pumpAndSettle function waits for the animation to finish. But the loading indicator is shown when the user taps the login button. And pump and settles keeps on working till loading indicator is present. When the expect function checks for the loading indicator it is not present in the widget tree. So the test fails.

To fix this we will use pump function. This function takes a duration as an argument. The duration is the time you want to wait for the animation to finish.

  await tester.enterText(emailTextField, 'abcd@mail.com');
  await tester.enterText(passwordTextField, 'abcd1234');

  await tester.tap(loginButton);
  await tester.pump(const Duration(seconds: 1));
  expect(find.byKey(loginCircularProgressKey), findsOneWidget);

  await tester.pump(Duration(seconds: 1));
  expect(find.byKey(loginCircularProgressKey), findsNothing);

he above test will passes. We also check if loading indicator is not present in the widget tree after the animation is finished.

Navigating to Todo Pages

The App navigates automatically to the Todo page after the user logs in. So we will test if the App navigates to the Todo page after the users taps login button with correct credentials.

We will check for the All Todo page title to check if the App navigates to the Todo page. If the Todo page title is present in the widget tree it will pass the test else it will fail the test.

at the end of login test just write this code

 await tester.pumpAndSettle();
    var quotesPageTitle = find.text('Todo');
    expect(quotesPageTitle, findsOneWidget);

The test fails and the Fail message is not much clear. Lets debug the test and see why its failing.

Click on the debug button instead of run button. The test will run in debug mode and its gets easier to debug the test.

The error message that we see right now is Failed to load Todos And that is cause we are not mocking the Todo data. And todo page is making a real HTTP request to fetch the quotes. So we need to mock the Todos data.

We will be using overrides to mock the Todos data. The Overrides are used to override the dependencies in the widget tree. We will override the TodoService dependency with a mock object of TodoService class.

But first lets create Mock class like we did in Unit tests and add this code to the test file on the top. To create a Mock class of our TodoService class.

class MockTodoService extends Mock implements TodoService {}

Inside the test function create a mock object of the TodoService class.and pass this mock object to the overrides.

  MockTodoService mockTodoService = MockTodoService();
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          quotesNotifierProvider
              .overrideWith((ref) => TodoService(mockTodoService)),
        ],
        child: MaterialApp(
          home: LoginScreen(),
        ),
      ),
    );

    when(() => mockTodoService.getTodos())
      .thenAnswer((_) async => mockTodoService);
  await tester.pumpAndSettle();

run the test and it must pass now. Also we should be able to navigate to the Todo’s page. Thats it. We are done with testing the Login Screen.

Testing Todo Pages

Now let us start with testing the Todos. Head over test folder and create a new file named todos_page_test.dart. To test todo page first we will have to Mock the Todos so that we can populate the Todos list in the Todos page.

MockTodoService mockTodoService = MockTodoService();
   Widget createWidgetUnderTest() { 
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          quotesNotifierProvider
              .overrideWith((ref) => TodoService(mockTodoService)),
        ],
        child: MaterialApp(
          home: LoginScreen(),
        ),
      ),
    );
  }
}

Here we create a Function named createWidgetUnderTest which returns a widget. This function will be used to create the widget tree for the test. We will be calling this function inside the pumpWidget method.

 testWidgets('All Todos Widget Test', (WidgetTester tester) async {
    await tester.pumpWidget(createWidgetUnderTest());

We need to return mock Todos when getTodos() function is called. lets create function which will return mock Todos instead. We will adding a delay of two seconds to to depict the real world scenario. Add this function before createWidgetUnderTest function.

  void getTodosAdter2secondsDelay() {
    when(() => mockTodoService.getTodos()).thenAnswer((_) async {
      return await Future.delayed(
          const Duration(seconds: 2), () => mockTodoServiceForTesting);
    });
  }

void getTodosAdter2secondsDelay() {
    when(() => mockTodoService.getTodos()).thenAnswer((_) async {
      return await Future.delayed(
          const Duration(seconds: 2), () => mockTodoServiceForTesting);
    });
  }

Widget createWidgetUnderTest() { 
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          quotesNotifierProvider
              .overrideWith((ref) => TodoService(mockTodoService)),
        ],
        child: MaterialApp(
          home: LoginScreen(),
        ),
      ),
    );
  }

  getTodosAdter2secondsDelay();

  // Pump All Quotes screen
  await tester.pumpWidget(createWidgetUnderTest());

First we will start by cheking if we are on the correct screen. We will check for the Todos page title to check if the App navigates to the Todos page. If the Todos page title is present in the widget tree it will pass the test else it will fail the test.

expect(find.text('Todos'), findsOneWidget);

A simple find by text Todos can be used to check if we are on the correct screen.

Now lets test if the Todos are loading. We will check if the loading indicator is present in the widget tree. If the loading indicator is present in the widget tree it will pass the test else it will fail the test.

  await tester.pump(const Duration(seconds: 1));
  expect(find.byKey(todosCircularProgressKey), findsOneWidget);
  await tester.pumpAndSettle();
  expect(find.byKey(todosCircularProgressKey), findsNothing);

Here we also check if the loader indicator is removed from the widget tree after the todos are loaded.

Now lets test if the todos are loaded. We will check if the todos are present in the widget tree. If the todos are present in the widget tree it will pass the test else it will fail the test.

  expect(find.text('Test Todo 1'), findsOneWidget);
  expect(find.text('Test Todo 2'), findsOneWidget);
  expect(find.text('Test Todo 3'), findsOneWidget);

Here are done widget testing of the application. Lets see how we can reuse the code that we have written for Widget Testing as Integration testing.

Reusing Widget Test as Integration Test

The difference between widget test and interatoin test is that, widget test runs on Console and integration test runs on the device. Now we will learn about integration test and will be resuing the widget tests into integration test.

To reuse the widget test as integration test, we first let convert the widget test into a function. Then we call this function inside the integration test. Let us see how to do this.

Head over to login_widget_test.dart and create a function called as loginWidgetTest and move the test code inside this function.it will look like this.

Future<void> loginWidgetTest(
    WidgetTester tester) async {
      
 MockTodoService mockTodoService = MockTodoService();
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          quotesNotifierProvider
              .overrideWith((ref) => TodoService(mockTodoService)),
        ],
        child: MaterialApp(
          home: LoginScreen(),
        ),
      ),
    );

    when(() => mockTodoService.getTodos())
      .thenAnswer((_) async => mockTodoService);
  await tester.pumpAndSettle();


  final loginText = find.byKey(loginScreenTextKey);
  final emailTextField = find.byKey(emailTextFormKey);
  final passwordTextField = find.byKey(passwordTextFormKey);
  final loginButton = find.byKey(loginButtonKey);

  expect(loginText, findsOneWidget);
  expect(emailTextField, findsOneWidget);
  expect(passwordTextField, findsOneWidget);
  expect(loginButton, findsOneWidget);



  await tester.enterText(emailTextField, 'abcd');
  await tester.enterText(passwordTextField, '1234');

  await tester.tap(loginButton);

  await tester.pumpAndSettle();
  final emailErrorText = find.text(kEmailErrorText);
  final passwordErrorText = find.text(kPasswordErrorText);

  expect(emailErrorText, findsOneWidget);
  expect(passwordErrorText, findsOneWidget);



  await tester.enterText(emailTextField, 'abcd@mail.com');
  await tester.enterText(passwordTextField, 'abcd1234');

  await tester.tap(loginButton);

  await tester.pump(const Duration(seconds: 1));
  expect(find.byKey(loginCircularProgressKey), findsOneWidget);


  await tester.pump(Duration(seconds: 1));
  expect(find.byKey(loginCircularProgressKey), findsNothing);
 
  expect(emailErrorText, findsNothing);
  expect(passwordErrorText, findsNothing);

  await tester.pumpAndSettle();

  var todosPageTitle = find.byKey(todosTextKey);
  expect(todosPageTitle, findsOneWidget);

}

We will call this function inside the main function.

void main(){
    testWidgets('Login Widget Test', loginWidgetTest);
}

Run the login widget test just to check if our refactor has not broken anything.

Just like we refactored the login widget test, we have to do the same for the todos widget test.

void main() {
  testWidgets('All Quotes Widget Test', allQuotesWidgetTest);
}

Future<void> allTodosWidgetTest(WidgetTester tester) async {
  MockTodoService mockTodoService = MockTodoService();

void getTodosAdter2secondsDelay() {
    when(() => mockTodoService.getTodos()).thenAnswer((_) async {
      return await Future.delayed(
          const Duration(seconds: 2), () => mockTodoServiceForTesting);
    });
  }

Widget createWidgetUnderTest() { 
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          quotesNotifierProvider
              .overrideWith((ref) => TodoService(mockTodoService)),
        ],
        child: MaterialApp(
          home: LoginScreen(),
        ),
      ),
    );
  }

  getTodosAdter2secondsDelay();

  // Pump All Todos screen
  await tester.pumpWidget(createWidgetUnderTest());

  expect(find.text('Todos'), findsOneWidget);

  await tester.pump(const Duration(seconds: 1));
  expect(find.byKey(todosCircularProgressKey), findsOneWidget);
  await tester.pumpAndSettle();
  expect(find.byKey(todosCircularProgressKey), findsNothing);

  expect(find.text('Test Todo 1'), findsOneWidget);
  expect(find.text('Test Todo 2'), findsOneWidget);
  expect(find.text('Test Todo 3'), findsOneWidget);
}

Now that we have refactored the widget tests, we can reuse them in the integration test.

Create a new folder called as integration_test inside the root folder. Inside this folder create a new file called as **integration_test.dart**.

Event the integration test in written inside the main function so lets create a main function.

void main(){
    
}

We have to multiple widget tests to run in the integration test. So we will use the group function.

void main(){
    group('Integratoin', () {
        
    });
}

the group function runs multiple tests in a group. The first parameter is the name of the group and the second parameter is a function which contains the tests. In this group we will run the login widget test and the quotes widget test.

   testWidgets('Login-Page', (tester) => loginWidgetTest(tester));
   testWidgets('All-Todo-Page', (tester) => allTodosWidgetTest(tester));

before running the integration test, we have to make sure that our Flutter widgets are ready. So we have to call the WidgetsFlutterBinding.ensureInitialized() function before running the integration test. Finally it will look like this.

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  group('Integration Tests', () {
 testWidgets('Login-Page', (tester) => loginWidgetTest(tester));
   testWidgets('All-Todo-Page', (tester) => allTodosWidgetTest(tester));
  });
}

Now to run integration test, we need a device to run. So open emulator, simulator or just connect your device to the computer and run the following command.

flutter test integration_test/integration_tests.dart

The integration test will run on the device and you can see the the app running on the device.

This is how we can reuse the widget tests as integration tests which saves a lot of time and effort

Here I conclude the Fourth article from Master Testing in Flutter series. In this article, I explained how you can Test the Widgets rendered on UI mock data with the Mocktail package. Reuse Widget Test as Integration test to save time.

This also concludes the Widget and Integration Testing part of our Testing Series. In the next article. I will cover Generate Goldens, and concepts of testing a widget.

If you want to check out the rest of the blogs from the testing Series here is a list of all the blogs present. I would still address if you follow this guide step by step.

Thank You for reading my blog, if you have any doubts or recommendations please let me know in the comment section below. I would like to know what are the other way to globally listen to network connectivity.

You have 50 Claps per day. Claps do Motivate us to keep writing. Gotta Use Them All

Tags

Leave A Comment