Testing in Flutter: automated testing, integration testing, ui testing
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.
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
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
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!