Skip to content

Instantly share code, notes, and snippets.

@Rex-Ferrer
Last active December 9, 2023 16:42
Show Gist options
  • Save Rex-Ferrer/c0d4bff4b0d3909daf8994410cd659ce to your computer and use it in GitHub Desktop.
Save Rex-Ferrer/c0d4bff4b0d3909daf8994410cd659ce to your computer and use it in GitHub Desktop.
16 Tips for Widget Testing in Flutter
// https://gist.github.com/Esgrima/c0d4bff4b0d3909daf8994410cd659ce
// https://dartpad.dev/c0d4bff4b0d3909daf8994410cd659ce
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:boolean_selector/boolean_selector.dart';
// (TODO: Tip # 1) Consider making frequently used variables/values constants
const _fooConst1 = '';
const _fooConst2 = '';
const _fooConst3 = '';
const _deskTopSize = Size(1920, 1040); // physical pixels
// (TODO: Tip # 2) Control which platforms your tests run on
// This is why we need to import the boolean_selector package
@TestOn(android && ios && chrome)
void main() {
// Make good use of [setUpAll], [setUp], [tearDownAll], and [tearDown]
// to avoid repetitive code. D.ont R.epeat Y.ourself.
// (TODO: Tip # 3a) Consider pre-loading fonts if you're using custom fonts
// Remember this runs once before ALL tests or groups
setUpAll(() async {});
// (TODO: Tip # 3b)
// Remember this runs before EACH test or group
setUp(() async {
final TestWidgetsFlutterBinding binding =
TestWidgetsFlutterBinding.ensureInitialized();
// (TODO: Tip # 4) Consider configuring your default screen size here.
// You can reset it to something else within a test
binding.window.physicalSizeTestValue = _deskTopSize;
});
// (TODO: Tip # 3c)
// Remember this runs once after ALL tests or groups
tearDownAll(() async {});
// (TODO: Tip # 3d)
// Remember this runs after EACH test or group
tearDown(() async {
// Code that clears caches can go here
});
group('Integration Tests', () {
// (TODO: Tip # 5a) What's this doing here? I didn't know this either,
// but you can call this within your groups and it'll run once before ALL
// your tests in the group
setUpAll(() async {});
// (TODO: Tip # 5b) Same as above but before EACH test
// This may be a perfect place for you to pump a fresh widget instead
// of doing each time in your testWidgets()
setUp(() async {
// For example, each test could start at the FooHomePage
await tester.pumpWidget(
FooHomePage(
showDebugBanner: false,
),
);
});
// (TODO: Tip # 5c) Same as above but after ALL tests
tearDownAll(() async {});
// (TODO: Tip # 5d) Same as above but after EACH test
tearDown(() async {});
// (TODO: Tip # 6) Attempt to write an integration widget test
// Share what you came up with in the comments
testWidgets('Foo Integration Test 1', (tester) async {
// This tests the Counter app in https://dartpad.dev/b6409e10de32b280b8938aa75364fa7b
await tester.pumpWidget(MyApp);
await tester.tap(find.byTooltip(‘Increment’));
await tester.pumpAndSettle();
// Assume this Text widget:
// Text(
// '$_counter',
// style: Theme.of(context).textTheme.headline4,
// semanticsLabel: ‘Counter’,
// ),
final Text text = find.BySemanticsLabel('Counter')
.last
.evaluate()
.single
.widget;
expect(text.data, ‘1’);
// Summary: You verified the behavior of the FloatingActionButton, Text, Scaffold, and
// MyHomePage widgets
});
// Here's an integration test similar to what I did for my project
testWidgets('Navigation Integration Test', (tester) async {
// FooHomePage is a widget that consists of a MaterialApp
await tester.pumpWidget(
FooHomePage(
showDebugBanner: false,
),
);
// Any subset of the following two statements may be required
// if you want to make assertions, take screenshots along the way,
// or perform any gestures. Some animations require both and a provided
// duration.
// Resources to read:
// 1. https://medium.com/flutter/event-loop-in-widget-tester-50b3ca5e9fc5
// 2. https://flutter.dev/docs/cookbook/testing/widget/introduction#4-build-the-widget-using-the-widgettester
// Pump triggers a frame sequence (build/layout/paint/etc), then flushes
// microtasks.
// A microtask is anything that is scheduled for execution in the event loop.
// Calling pump() repeatedly flushes microtasks and allows new ones to be
// created.
await tester.pump();
// In widget tests, a new frame is only rendered when pump() is called. If you call
// pumpAndSettle(), frames will be requested until an animation has finished.
await tester.pumpAndSettle(duration: Duration(milliseconds: animationDuration));
// Let's navigate to the ItemPage. I've made an assumption there's a button
// widget with the text 'ItemPage'
await tester.tap(find.text('ItemPage'));
await tester.pumpAndSettle();
// Let's navigate to the Details page. Similar assumption as above
await tester.tap(find.text('Details'));
await tester.pumpAndSettle();
// Let's navigate back to the home page
// Only use this function if you have a back button with a 'Back' tooltip or
// it'll throw an error
await tester.pageBack();
await tester.pump();
await tester.pumpAndSettle();
await tester.pageBack();
await tester.pump();
await tester.pumpAndSettle();
});
});
// (TODO: Tip # 7) Review the flutter_test documentation thoroughly, so you have an idea
// of what's at your disposal and what you need to implement.
group('ItemPage', () {
testWidgets('testFooBehavior1', (tester) async {
// (TODO: Tip # 8) Need a widget to be visible when it is offscreen in a
// ScrollView?
// Use ensureVisible and set skipOffStage to false if the widget
// could be offStage
await tester.ensureVisible(find.byType(Button, skipOffStage: false));
await tester.pumpAndSettle();
});
testWidgets('testFooBehavior2', (tester) async {
// (TODO: Tip # 9) Learn how to take screenshots
// Here's an example screenshot comparison
await expectLater(
find.text('Save'),
matchesGoldenFile('save.png'),
);
// (TODO: Tip # 10) Use tags to limit which tests are run.
// If you're having text scaling bugs all of sudden, then pass this
// command line flag to only run tests related to 'text-scaling'
// --tags 'text-scaling'
// Pro-Tip: Create a constants folder and have a tags.dart holding all your tags
}, tags: ['contrast-ratio', 'text-scaling']);
});
group('ItemDetailsPage', () {
// (TODO: Tip # 11a) Know your gestures
// These are the default gestures provided to you by flutter_test
// 1. drag, dragFrom
// 2. fling, flingFrom
// 3. tap, tapAt
// 4. longPress, longPressAt
// 5. press
// (TODO: Tip # 11b) Create custom gestures
// Custom scroll function
//
// final gesture = await createGesture();
//
// // This is like placing your finger on the screen
// await gesture.down(
// getCenter(
// find.byType(CustomScrollView),
// ),
// );
//
// // This is like moving your finger up or down the screen
// await gesture.moveBy(
// Offset(0, numberOfPixels),
// );
//
// // This is like lifting your finger off the screen and
// // ends the gesture. This could be crucial because in some instances
// // future gestures won't be recognized and you may have errors in
// // your screenshots.
// await gesture.up();
//
// await pumpAndSettle();
testWidgets('FooBehavior1', (tester) async {
// (TODO: Tip # 12) When using a [Finder], are you running into the too many
// elements StateError?
// (TODO: Tip # 12a) General strategy is to identify probable unique
// identifiers like Keys or Tooltips
// For example: find.byKey(Key(‘Toggle’))
// (TODO: Tip # 12b) You can also use a ChainedFinder to step through the
// matching process
final Key key1 = Key();
await tester.pumpWidget(
// (TODO: Tip #13) Minimize boilerplate code with helpers
// Widget _directionalityBoilerplate(Widget child) {
// return new Directionality(
// textDirection: TextDirection.ltr,
// child: child,
// );
// }
_directionalityBoilerplate(Column(
children: [
Container(
key: key1,
child: const Text('1'),
),
Container(
child: const Text('2'),
)
],
)),
);
// ChainedFinder
final Text text = find
.descendant(
// This is the parent or first widget that gets found
of: find.byKey(key1),
// Text is searched for in the found widget
matching:find.byType(Text),
)
.last
.evaluate()
.single
.widget;
expect(text.data, '1');
// (TODO: Tip # 14) Customize platform-specific configurations
// Give a pesky iPad Air extra wiggle-room to run before timing out
}, onPlatform: {'ios': const Timeout.factor(2)});
testWidgets('FooBehavior2', (tester) async {
// (TODO: Tip # 15) Skip tests if an issue is causing it to fail
}, skip: 'Due to bug X. See <link>');
});
}
// (TODO: Tip # 16) Widget tests are great opportunities to use Dart
// extension methods, available since Dart 2.7. (https://dart.dev/guides/language/extension-methods)
extension SwitchDarkMode on WidgetTester {
// As an extension method, your code in testWidget() will look like this:
//
// await tester.switchToDarkMode();
//
// Rather than:
// .await switchToDarkMode(tester);
//
// The only benefits are readability and consistency in your code, and you could
// write less code.
Future<void> switchToDarkMode() {
// Does things
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment