The success of any application depends on its quality. For customers to love an app and evangelize it via word-of-mouth advertising, it must provide the highest quality possible and withstand adverse conditions.
Quality assurance plays an important role in addressing an application’s defects before it reaches production. Almost all software teams have some form of QA as part of their development lifecycle, even if there is no dedicated QA team that only does this job.
It’s the nature of software engineering that new features are built on top of the existing codebase. Hence, whoever is responsible for QA will have to test not only the new features, but the existing features as well to ensure the app works nicely with the new features integrated.
Now the problem is: the time spent in QA will increase with every new feature, and there is a very real chance that not everything will be well-tested. Bugs can easily slip into the user’s hand.
Automation testing really helps here by automating some of the work that QA would do manually. We can write an automation test for those features that QA has already tested so the team can focus on testing new features while the old features will be tested automatically. This saves a lot of time and brings a higher level of confidence in shipping the app to production.
In this tutorial, we’ll introduce automated testing for Flutter and review how to write each type of automation test with an example.
Here are the three types of tests we’ll cover:
Let’s have a look at the sample app we’ll be testing:
For the purposes of this tutorial, our requirement is that the list of all products should be available on the app homepage. The user can add the product to the cart by clicking the cart icon beside it. Once added, the cart icon should be changed.
Clicking on the Cart text should open up a cart page, which displays a list of all the products added to the cart. The products can be removed from the cart either via the cancel button or a swipe to dismiss.
As discussed above, we’ll automate three types of tests for our Flutter app: unit tests, widget tests, and integration tests. An app can have several combinations of these three tests, but it’s up to you to design and implement the tests in a way that provides the most confidence for your use case.
Let’s begin with the unit test for the app. This tests the single method of the class by ensuring the method provides the expected result based on the input given to it. It helps you to write more testable and maintainable code.
Our goal is to write unit tests for the Cart
class — to be more specific, we will make sure that adding and removing methods for products gives the correct result.
First, dd the test dependency:
dev_dependencies: test: ^1.14.4
Here is the Cart
class, which has methods to add and remove items:
import 'package:flutter/material.dart'; /// The [Cart] class holds a list of cart items saved by the user. class Cart extends ChangeNotifier { final List<int> _cartItems = []; List<int> get items => _cartItems; void add(int itemNo) { _cartItems.add(itemNo); notifyListeners(); } void remove(int itemNo) { _cartItems.remove(itemNo); notifyListeners(); } }
Next, we’ll create a file to write test cases. Inside the test
folder (at the root of the project), create a new file cart_test.dart
. It should look something like this:
Now add the below code inside it:
N.B., make sure to give a test file named as
(classToTest)_test.dart
.
import 'package:flutterdemos/testingdemo/models/cart.dart'; import 'package:test/test.dart'; void main() { group('Testing Cart class', () { var cart = Cart(); //Test 1 test('A new product should be added', () { var product = 25; cart.add(product); expect(cart.items.contains(product), true); }); // Test 2 test('A product should be removed', () { var product = 45; cart.add(product); expect(cart.items.contains(product), true); cart.remove(product); expect(cart.items.contains(product), false); }); }); }
Here, Test 1 verifies the added item should exist in the cart list, and Test 2 checks whether the removed item does not exist in the cart. The expect()
method is a way to validate our output with expectation.
Now we’ll run the unit test. Simply hit the play button in the IDE.
You can also try with the terminal using the following command:
flutter test test/cart_test.dart
As its name suggests, the widget test focuses on a single widget. Unlike the unit test, the widget test makes sure that a particular widget is looking and behaving as expected. You should write a widget test for at least all common widgets.
Our goal here is to write a widget test to ensure that the homepage is working as expected.
First, add one more test dependency:
dev_dependencies: test: ^1.14.4 # for unit test flutter_test: sdk: flutter
Similar to the cart_test.dart
file we created in the previous section, we’ll now create one more file home_test.dart
inside the test
folder. Let’s add the below code to it.
Widget createHomeScreen() => ChangeNotifierProvider<Cart>( create: (context) => Cart(), child: MaterialApp( home: HomePage(), ), ); void main() { group('Home Page Widget Tests', () { // Test 1 testWidgets('Title should be visible', (tester) async { await tester.pumpWidget(createHomeScreen()); expect(find.text('Shopping App Testing'), findsOneWidget); }); }); }
The methods below are the building blocks for writing our widget test:
createHomeScreen()
– provides the UI for the home screen that we would normally do in the main.dart
filetestWidgets()
– creates the WidgetTester
that provides ways to interact with the widget being testedawait tester.pumpWidget()
– renders the provided widgetfind.text()
– finds the widget with the given text. Sometimes we may have the same text in the UI, so find.byKey(Key('string'))
becomes really helpfulexpect()
– takes the found widget and compares it with the expected Matcher
, which can be findsOneWidget
, findsNothing
, etc.Let’s cover a couple other important test cases that we would otherwise have to perform manually. Here, we test that the product list is visible on the homepage:
testWidgets('Product list should be visible', (tester) async { await tester.pumpWidget(createHomeScreen()); expect(find.byType(ListView), findsOneWidget); });
And here, we test that the user is able to scroll the product list:
testWidgets('Scroll test', (tester) async { await tester.pumpWidget(createHomeScreen()); expect(find.text('Product 0'), findsOneWidget); await tester.fling(find.byType(ListView), Offset(0, -200), 3000); await tester.pumpAndSettle(); expect(find.text('Product 0'), findsNothing); });
A full list can be found here.
Now run the test.
Integration tests help to achieve end-to-end testing for the app. They enable us to understand whether users are able to complete the full flow of the app. It’s essentially like testing a real application.
Unlike unit tests and widget tests, integration tests run on a real device, so we get a chance to see how tests are being performed. In a perfect world, we’d write and run as many tests as we need. But if we have limited time, we should absolutely write an integration test at the very least.
Our goal here is to test that the user is able to add and remove products to and from the cart. Here’s the dependency required for the integration test:
dev_dependencies: test: ^1.14.4 # for unit test flutter_test: # for widget test sdk: flutter flutter_driver: sdk: flutter integration_test: ^1.0.1
Now we create the integration_test
folder at the project root and add a file driver.dart
inside it with the following code:
import 'package:integration_test/integration_test_driver.dart'; Future<void> main() => integrationDriver();
Then we’ll create a file app_test.dart
and add the below code:
void main() { group('Testing full app flow', () { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('Add product and remove using cancel button', (tester) async { await tester.pumpWidget(MyApp()); //Add await tester.tap(find.byIcon(Icons.shopping_cart_outlined).first); await tester.pumpAndSettle(Duration(seconds: 1)); expect(find.text('Added to cart.'), findsOneWidget); // Move to next page await tester.tap(find.text('Cart')); await tester.pumpAndSettle(); //Remove via cancel button await tester.tap(find.byKey(ValueKey('remove_icon_0'))); await tester.pumpAndSettle(Duration(seconds: 1)); expect(find.text('Removed from cart.'), findsOneWidget); }); }); }
As we can see in the code above, there are instructions to perform actions and verify the results, just as we would do manually:
await tester.tap()
– clicks on the specified widgetawait tester.pumpAndSettle()
– when users click on a UI element, there might be an animation. This method ensures that the animation has settled down within a specified duration (e.g., if we think the required widget is not yet available), after which period we’re good to go for new instructionsWe also have a provision for removing products via swipe. The code for achieving this behavior goes here:
//Remove via swipe await tester.drag(find.byType(Dismissible), Offset(500.0, 0.0)); await tester.pumpAndSettle(Duration(seconds: 1)); expect(find.text('Removed from cart.'), findsOneWidget);
Finally, we’ll run the test on a real device. Run following command in the terminal:
flutter drive — driver integration_test/driver.dart — target integration_test/app_test.dart
In this tutorial, we’ve introduced automation testing in Flutter and walked through the various types of tests we can write via examples. You can view the complete source code with all the test cases in my GitHub.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
3 Replies to "Automated testing in Flutter: An overview"
Whole blog is very informative but i want to know that can we discard multiple card in once?
It depends on your design. For this particular use case you can probably loop the discarding behaviour.
How to handle system default permission in code ?