Integration tests allow for tests to execute on Android and iOS. This allows us to ensure platform specific behaviour (native ably-flutter code and dependencies, ably-cocoa and ably-java) are behaving as expected.
This project has only one integration test target, main.dart
.
To run all tests:
- run
flutter drive --target lib/main.dart
, or - run
flutter drive
which runs them all (one and only,main.dart
). - Android Studio / VS Code: Run the
Integration Test
run configuration. It is a compound run configuration which runs bothIntegration Test Driver
andIntegration Test App
.
Test modules include basicTests
and realtime
(For the full list, TestModules
in tests_config.dart). To run one module (a collection of test groups), either:
- In Android Studio:
- Modify the
Integration Test Driver
run/debug configuration by adding-m module_name_1,module_name_2
inProgram Arguments
.- Note: No spaces between module_names. Alternatively, specify the modules separately:
-m module_name_1 -m module_name_2
- Note: No spaces between module_names. Alternatively, specify the modules separately:
- Modify the
- Launch each application manually:
- Launch the integration test app manually
- Launch the driver and pass the modules with the
-m
option:dart test_integration/test_driver/runner.dart -m rest,realtime
To run a specific test group:
- In tests_config.dart, comment out the test group key: value pairs which you don't want to run.
- Launch both integration test app and driver:
- Launch
Integration Test
compound run configuration in Android, or - Launch
Integration Test App
andIntegration Test Driver
run configurations, or - Launch each application manually:
- Launch the integration test app manually
- Launch the driver:
dart test_integration/test_driver/runner.dart
- Launch
Run the bash script in the project root directory
cd test_integration
./run_integration_tests.sh
cd test_integration
flutter driver test_driver/app.dart
When
flutter drive test_driver/*
is executed, flutter drive
searches of pairs of files in the
directory test_driver/
with the names xxx.dart
and xxx_test.dart
where xxx.dart
is the app to be run on the device or emulator and
xxx_test.dart
the unit-test like test that controls the app by
sending commands and receiving responses that are then evaluated with
expect(...)
and similar to unit tests.
flutter drive
then executes the app in the found devices (or a
device or emulator specified in additional parameters) and
the test on the host platform where flutter drive
is executed.
Because it is often convenient to execute the test application like a
normal application (without flutter drive
), the app implementation
is put into lib/
instead of test_driver
.
To satisfy flutter drive
the xxx.dart
file has to exist, but all
it does is to import and calls the main()
function of the app in
lib/main.dart
.
It is possible to have multiple applications in lib/
and different
test_driver/xxx.dart
files can call different main functions from
lib/
.
flutter drive
provides a set of functions to simulate user input
like touch, but here we discuss only non-UI drive tests.
The test needs to establish a connection with the application by
executing driver = await FlutterDriver.connect();
.
Tests send a message to the app using
final result = await driver.requestData(message.toJson());
Only String
values are supported as message content, therefore we
use JSON serialized strings.
enableFlutterDriverExtension(handler: dataHandler);
This code needs to be executed before runApp()
.
DriverDataHandler
is a helper class that deserializes received
messages and serializes responses.
TestControlMessage
is another helper class that ensures the message
conforms to some minimal structure. For example that a test name is
passed.
It additionally supports a Map<String, dynamic>
as payload
that
allows any JSON-serializable value.
TestDispatcher
is a minimal application widget that invokes tests
depending on the name in the received message.
A mapping from test name to a test widget factory needs to be passed
to lib.main(...)
.
Example:
final testFactory = <String, TestFactory>{
TestName.platformAndAblyVersion: (d) => PlatformAndAblyVersionTest(d),
TestName.appKeyProvisioning: (d) => AppKeyProvisionTest(d),
};
void main() => app.main(testFactory);
When test code in the app is done, it needs to call completeTest()
with the response data to notify the test about the result.
The response data again needs to be a JSON-serializable Map<String, dynamic>
.
Exceptions need to be handled by the test code in the app and
communicated to the test using the completeTest()
method.
If an app contains multiple tests, they will have some dependency between them, because the app is not restarted for each test which results for example in the plugin to keep the state from the previous test.
To have truly independent tests, each test needs to have its own
xxx.dart
and xxx_test.dart
file pair in the test_driver/
directory.
They can all call the same lib/main.dart
though, because
for each xxx_test.dart
file the app will be closed and restarted.
Currently sending a message from the driver test to the app starts a new test and completing a test sends a response.
It would be possible to support multiple messages per test in both directions, but it is not yet clear if this is actually required.
This is an attempt to have a clean structure in apps with multiple tests. Time should tell if this is a good approach.
See https://medium.com/flutter-community/hot-reload-for-flutter-integration-tests-e0478b63bd54 for how to improve development performance by utilizing Hot Reload/Restart for Driver tests