Integration Testing
Summary
In this lesson we'll cover:
- Integration testing concepts
- Working with the
flutter_driver
package - Working with widget keys
- Writing an integration test
The Code for This Lesson
You can check out the step/step09
branch here which will contain the code for this lesson.
Core Concepts
- Integration tests test how pieces of your application work together as a whole.
- A typical integration test runs the actual app in an automated fashion and simulates the user interacting with the app.
How Integration Tests Work in Flutter
- With an integration test, one bit of code will run (or "drive") the actual app and another separate bit of code will simulate user interaction.
- The
flutter_driver
package will run or "drive" our app for us.
"Driving" Our Application
- We'll use the
flutter_driver
package to start our app.
Adding flutter_driver
- Add the following to your
pubspec.yaml
file:
# pubspec.yaml
# ...
dev_dependencies:
# ...
flutter_driver:
sdk: flutter
# ...
Adding test/test_driver/app.dart
- All files for integration tests will resides in a new directory,
test/test_driver
flutter_driver
is initiated within a newmain()
function within a new file calledtest/test_driver/app.dart
flutter_driver
then will invoke themain()
function of our app located inpackage:tourismandco/main.dart
:
// test/test_driver/app.dart
import 'package:flutter_driver/driver_extension.dart';
import 'package:tourismandco/main.dart' as app;
void main() {
// This line enables the extension
enableFlutterDriverExtension();
// Call the `main()` function of your app or call `runApp` with any widget you
// are interested in testing.
app.main();
}
NOTE: Original source here.
Writing an Integration Test
flutter_driver
will provide a handy command to run thistest/test_driver/app.dart
file:flutter drive --target=test_driver/app.dart
- It will run any test files located in the same directory.
- So now let's write an actual test file (ending with
_test.dart
), createtest/test_driver/app_test.dart
. - But not so fast, read on.
Before We Continue
- Our tests will need to see if specific widgets exist with specific bits of text, such as a location name.
- How will be "find" these widgets then?
- What we will do is add a special property to certain widgets, called a "key".
Understanding What Widget Keys Are
- Widgets can be uniquely identified in the "widget tree" by a "key".
- The "widget tree" is a hierarchical representation of a given screen in a Flutter app.
- A widget tree is akin to the "DOM" in HTML. This tree structure allows Flutter to build a tree of widgets in memory.
- When Flutter needs to update specific parts of the UI, representing the UI as a "tree" makes it easy to do this.
- Therefore, a "key" is a unique string associated with the widget.
- The "key" is effectively say, the equivalent to the "id" property of a in HTML.
- Keys can be used to optimize the performance of an app in certain situations, but are generally optional.
- For our purposes, we can use a "key" that are tests can use as well.
- NOTE: many integration testing frameworks for the web work the same way\
Adding Widget Keys for our Test
- Locate
lib/screens/locations/locations.dart
- Add the following key to the existing code:
// lib/screens/locations/locations.dart
// ...
// NOTE _itemBuilder() is can existing method you'll have implemented in this file,
// so you're just adding the 'key' property here...
Widget _itemBuilder(BuildContext context, Location location) {
return GestureDetector(
// ...
key: Key('location_list_item_${location.id}'),
// ...
- Here, each widget returned by our
_itemBuilder()
is now uniquely identifyable in the Flutter widget tree. - Next, update the folowing file to add another key to our
LocationTile
widget:
// lib/widgets/location_tile.dart
// ...
@override
Widget build(BuildContext context) {
// ...
return Container(
// ...
children: [
Text(
// ..
key: Key('location_tile_name_${location.id}'),
// ...
- Here, you see we have another uniquely named key for the specific
Text
widget in ourLocationTile
.
Implemeting our Integration Test
- We can now write our test, which will assume our app is already running via our "driver", implemented earlier in this tutorial.
- We will fetch all locations and wait until the
Locations
screen is loaded. - The test will do a basic check to see if the location list is loaded, by finding a widget by the key we defined in the previous step.
// test/test_driver/app_test.dart
// Imports the Flutter Driver API
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
import 'package:tourismandco/models/location.dart';
void main() {
group('happy path integration tests', () {
final locations = Location.fetchAll();
// First, define the Finders. We can use these to locate Widgets from the
// test suite. Note: the Strings provided to the `byValueKey` method must
// be the same as the Strings we used for the Keys in step 1.
final locationListItemTextFinder =
find.byValueKey('location_list_item_${locations.first.id}');
FlutterDriver driver;
// Connect to the Flutter driver before running any tests
setUpAll(() async {
driver = await FlutterDriver.connect();
});
// Close the connection to the driver after the tests have completed
tearDownAll(() async {
if (driver != null) {
driver.close();
}
});
test('a location name appears in location list', () async {
// Use the `driver.getText` method to verify the counter starts at 0.
expect(await driver.getText(locationTileOverlayTextFinder), isNotEmpty);
});
// NOTE one more test to come in the next step!
});
}
Running Our Integration Test
- Since we are using a custom command to run our integration test,
flutter drive <...>
, we can't just right click this test file and expect it to run. - We can configure a custom command in Visual Studio Code to run these tests but that's outside the scope of this tutorial, so we will run it via the command line.
- Open a command prompt via the Terminal app in macOS or the Command Prompt in Windows.
- Ensure you have
flutter
set in yourPATH
environment variable. If you do not, check out my previous video on how to set up Flutter on macOS or Windows. - Ensure an iOS Simulator or Android Emulator is running.
- Now run
cd <PATH TO YOUR tourismandco app>
thenflutter drive --target=test_drive/app.dart
- The integration test should now run.
Adding One Additional Test Case
- As you can see, we created a "finder" which allowed us to find a widget and make a test assertion.
- Let's add another test case, this time ensuring that we can tap on a specific location, transition to the Location Detail screen and that specific text there appears properly:
// test/test_driver/app_test.dart
// ...
void main() {
group('happy path integration tests', () {
// ...
final locationTileOverlayTextFinder =
find.byValueKey('location_tile_name_${locations.first.id}');
// ...
test('a location in the list is tappable', () async {
// First, tap on the button
await driver.tap(locationTileOverlayTextFinder);
// Use the `driver.getText` method to verify the counter starts at 0.
expect(await driver.getText(locationTileOverlayTextFinder), isNotEmpty);
});
// ...
- Here, we are adding an additional finder and test case in the same
group()
test case we've implemented in the previous step. - Re-run the test to ensure everything passes correctly.
Summary
- In this tutorial, we learned a very important part to app testing which is integration testing.
- Integration tests ensure that each major piece our app works together without any crashes, failure to load data, order incorrect rendering issues.
Sources
- For the original documentation on this, click here.