Effective Patrol
Over the past months, we've written many Patrol tests and often learned the hard way what works well and what doesn't. We're sharing our findings hoping that they'll help you write robust tests.
PREFER using keys to find widgets
Patrol's custom finders are very powerful, and you might often be inclined to find the widget you want in a variety of ways. While we're encouraging you to explore and play with Patrol's custom finders, we are quite confident that keys are the best way to find widgets.
Why not strings?
At first, strings might seem like a good way to find widgets.
They'll get increasingly annoying to work with as your app grows and changes, for example, when the strings in your app change.
Using strings stops making any sense when you have more than 1 language in your app. Using strings in such case is asking for trouble.
Why not classes?
There are 2 problems with using classes.
First is that they hurt your test's readability. You want to tap on the login button or enter text into the username field. You don't want to tap on, say, the third button and enter text into the second text field.
The second problem is that classes are almost always an implementation detail.
As a tester, you shouldn't care if something is a TextButton
or an
OutlineButton
. You care that it is the login button, and you want to tap on
it. In most cases, that login button should have a key.
Let's consider this simple example:
await $(LoginForm).$(Button).at(1).tap(); // taps on the second ("login") button
This works, but the code is not very self-explanatory. To make it understandable at glance, you had to add a comment.
But if you assigned a key to the login button, the above could be simplified to:
await $(#loginButton).tap();
Much better!
Let's see another example:
await $(Select<String>).tap(); // taps on the first Select<String>
If the type parameter is changed from String
to, for example, some specialized
PersonData
model, that finder won't find anything. You'd have to update it to:
await $(Select<PersonTile>).tap();
You had to change your test, even though nothing changed from the user's perspective. This is usually a sign that you rely too much on classes to find widgets.
This whole section could be summed up to the simple maxim:
Have tester's mindset.
Treat your finders as if they were the tester's eyes.
CONSIDER having a file where all keys are defined
The number of keys will get bigger as your app grows and you write more tests.
To keep track of them, it's a good idea to keep all keys in, say,
lib/keys.dart
file.
import 'package:flutter/foundation.dart';
typedef K = Keys;
class Keys {
const Keys();
static const usernameTextField = Key('usernameTextField');
static const passwordTextField = Key('passwordTextField');
static const loginButton = Key('loginButton');
static const forgotPasswordButton = Key('forgotPasswordButton');
static const privacyPolicyLink = Key('privacyPolicyLink');
}
Then you can use it in your app's and tests' code:
@override
Widget build(BuildContext context) {
return Column(
children: [
/// some widgets
TextField(
key: K.usernameTextField,
// some other TextField properties
),
// more widgets
],
);
}
void main() {
patrolTest('logs in', (PatrolIntegrationTester $) {
// some code
await $(K.usernameTextField).enterText('CoolGuy');
// more code
});
}
This is a good way to make sure that the same keys are used in app and tests. No more typos!
PREFER having one test path
Good tests test one feature, and test it well (this applies to all tests, not
only Patrol tests). This is often called the "main path". Try to introduce as
little condional logic as possible to help keep the main path straight. In
practice, this usually comes down to having as few if
s as possible.
Keeping your test code simple and to the point will also help you in debugging it.
DO add a good test description explaining the test's purpose
If your app is non-trivial, your Patrol test will become long pretty quickly. You may be sure now that you'll always remember what the 200 line long test you've just written does and are (rightfully) very proud of it.
Believe us, in 3 months you will not remember what your test does. This is why
the first argument to patrolTest
is the test description. Use it well!
// GOOD
import 'package:awesome_app/main.dart';
import 'package:patrol/patrol.dart';
void main() {
patrolTest(
'signs up for the newsletter and receives a reward',
($) async {
await $.pumpWidgetAndSettle(AwesomeApp());
await $(#phoneNumber).enterText('800-555-0199');
await $(#loginButton).tap();
// more code
},
);
}
// BAD
void main() {
patrolTest(
'test',
($) async {
await $.pumpWidgetAndSettle(AwesomeApp());
await $(#phoneNumber).enterText('800-555-0199');
await $(#loginButton).tap();
// more code
},
);
}