Testing in Flutter: automated testing, integration testing, ui testing

Andrew Frolov
6 min readMar 21, 2023

Hey there, Flutter enthusiasts!

Are you tired of manually testing your app every time you make a change? Do you want to avoid those embarrassing bugs that slip through the cracks?

Then automated testing is the way to go! Let me introduce you to the different types of testing in Flutter: automated testing, integration testing, and UI testing.

Automated Testing

Let's start with the basics: automated testing. This is where you write tests that check if your code works as expected. Automated tests run automatically, so you don't have to run them manually each time.

Flutter has a built-in test framework, which makes writing automated tests easy. Here's an example:

void main() {
test('addition', () {
expect(1 + 1, equals(2));
});

test('subtraction', () {
expect(2 - 1, equals(1));
});
}

This test checks if basic arithmetic operations work as expected. You can run this test using the flutter test command.

But what about more complex tests? You can use the flutter_driver package for that. Let’s take a look for more complex examples with it.

That feeling when you catch a bug with your Flutter automated tests

Navigation testing

One important part of UI testing is testing navigation between different screens in your app. Here’s an example of how you can use Flutter’s Navigator and PageRoute classes to test navigation:

testWidgets('Navigate to a new screen', (WidgetTester tester) async {
// Build the app
await tester.pumpWidget(MyApp());

// Tap the button that navigates to a new screen
await tester.tap(find.byKey(ValueKey('navigateButton')));
await tester.pumpAndSettle();
// Check that the new screen is displayed
expect(find.text('New Screen'), findsOneWidget);
});

In this example, we’re testing that tapping a button with a specific key (ValueKey('navigateButton')) navigates the user to a new screen. We use the pumpWidget method to build the app, and we use tester.tap to simulate a button press. We then use pumpAndSettle to wait for any animations to finish and for the new screen to be displayed. Finally, we use expect to check that the new screen is displayed.

Asynchronous code testing

Sometimes, you may need to test code that involves asynchronous operations such as network calls or animations. In such cases, you can use the expectLater method to test the expected behaviour of the code. Here is an example:

test('Fetch data from API', () {
expectLater(fetchDataFromAPI(), completion(isNotNull));
});

Future<String> fetchDataFromAPI() async {
// Make a network call and return data as a string
final response = await http.get(Uri.parse('https://example.com/data'));
return response.body;
}

In this example, we are testing a function fetchDataFromAPI that makes a network call and returns data as a string. We are using the expectLater method to check that the function returns a non-null value.

Integration Testing

Why we need integration tests?

Integration testing is where you test how different parts of your app work together. You can use the flutter_driver package to write integration tests that simulate user interactions.

Here’s an example:

void main() {
group('scrolling', () {
FlutterDriver driver;

setUpAll(() async {
driver = await FlutterDriver.connect();
});

tearDownAll(() async {
driver?.close();
});

test('scrolling down', () async {
final listFinder = find.byValueKey('list');
final initialFirstItem = await driver.getText(find.text('Item 0'));
await driver.scroll(listFinder, 0, -300, Duration(seconds: 1));
final finalFirstItem = await driver.getText(find.text('Item 0'));

expect(initialFirstItem, isNot(equals(finalFirstItem)));
});
});
}

User authentication testing

void main() {
group('Authentication Test', () {
FlutterDriver driver;

setUpAll(() async {
driver = await FlutterDriver.connect();
});

tearDownAll(() async {
if (driver != null) {
driver.close();
}
});

test('Log in with valid credentials', () async {
// Enter valid username and password
await driver.tap(find.byValueKey('username_field'));
await driver.enterText('valid_username');
await driver.tap(find.byValueKey('password_field'));
await driver.enterText('valid_password');

// Tap the login button
await driver.tap(find.byValueKey('login_button'));

// Verify that we are on the home screen
expect(await driver.getText(find.byValueKey('title')), 'Home');
});

test('Log in with invalid credentials', () async {
// Enter invalid username and password
await driver.tap(find.byValueKey('username_field'));
await driver.enterText('invalid_username');
await driver.tap(find.byValueKey('password_field'));
await driver.enterText('invalid_password');

// Tap the login button
await driver.tap(find.byValueKey('login_button'));

// Verify that we see an error message
expect(await driver.getText(find.byValueKey('error_message')), 'Invalid username or password');
});
});
}

In this example, we are using flutter_driver to test the authentication flow of an app. We simulate user input by entering a username and password using the driver.enterText method, tap the login button using the driver.tap method, and verify the expected behaviour by checking the text of widgets using the driver.getText method. We test both valid and invalid credentials.

UI testing

When you thought you had it all figured out, but then the UI tests proved you wrong

Ensures that your app’s user interface behaves as expected and that all the different elements of your app work together seamlessly.

In Flutter, there are a few different types of UI testing you can do: widget testing, integration testing, and end-to-end testing. Let’s take a closer look at each type.

Widget Testing

Widget testing is the most basic type of UI testing you can do in Flutter. It involves testing individual widgets in isolation to make sure they behave as expected. Widget testing is great for catching simple bugs and ensuring that your widgets are working as intended.

Here’s an example of a simple widget test using the flutter_test library:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());

// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);

// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();

// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Demo Home Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'0',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => {},
tooltip: 'Increment',
child: Icon(Icons.add),
),
),
);
}
}

In this example, we’re testing a simple app that displays a counter and allows the user to increment it with a button press. We use the tester.pumpWidget method to build the app, and then we use the find.text method to find specific widgets in the widget tree. We then use the tester.tap method to simulate a button press, and we use tester.pump to update the widget tree. Finally, we use the expect method to check that the counter has incremented correctly.

Text Input testing

Another important part of UI testing is testing text input. Here’s an example of how you can use Flutter’s TextFormField widget and the tester.enterText method to test text input:

testWidgets('Enter text in a TextFormField', (WidgetTester tester) async {
// Build the app
await tester.pumpWidget(MyApp());

// Enter text in the TextFormField
await tester.enterText(find.byType(TextFormField), 'hello');
await tester.pump();

// Check that the text has been entered
expect(find.text('hello'), findsOneWidget);
});

In this example, we’re testing that text entered in a TextFormField widget is displayed correctly. We use pumpWidget to build the app, and we use tester.enterText to simulate text input. We then use pump to update the widget tree, and we use expect to check that the text has been entered correctly.

Conclusion

Testing is must-have approach in your projects in any case. I saves you a lot of time on investigation and let you breath free when you’re doing any changes that could affect something. No rush here, just do it when you have complex logic or operations. It’s not necessary to have 100% coverage, but at least try it!

An of course let it be green :)

Thank’s for reading and subscribe!

--

--

Andrew Frolov

Software Engineer. Retail tech, marketplaces, ad-tech. My email — tendallas@ex.ua