Finder-Matcher Gen is an open-source project that allows you to generate custom Finder
and Matcher
classes using annotations.
It is a companion tool for writing widget and integration tests. Before diving into why you should use this tool, it is essential to understand what Finder
and Matcher
is in Flutter.
Finder searches a widget tree for widgets that meets certain requirements or pattern. If you've written widget or integration tests before, you've most likely used Finders. The function call find.byType(type)
and find.text('title')
uses an implementation of the Finder
class to locate widgets by type and text respectively.
Assuming you have a widget named TrafficLightWidget
that replicates an actual traffic light:
class TrafficLightWidget extends StatelessWidget {
const TrafficLightWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
TrafficLightLampWidget(
color: Colors.red,
text: 'STOP',
openBarricade: false,
),
TrafficLightLampWidget(
color: Colors.green,
text: 'GO',
openBarricade: true,
),
],
);
}
}
class TrafficLightLampWidget extends StatelessWidget {
const TrafficLightLampWidget({
super.key,
required this.color,
required this.text,
required this.openBarricade,
});
final Color color;
final String text;
final bool openBarricade;
@override
Widget build(BuildContext context) {
return Column(
children: [
CircleAvatar(radius: 40, backgroundColor: color),
Text(text),
],
);
}
}
Your widget test should assert that the red and green TrafficLightLampWidget
is contained in the widget tree.
In this scenario, using find.byType(TrafficLightLampWidget)
isn’t ideal, because two widgets with the same type are in the widget tree.
Without a custom finder, your find code will probably be written as:
testWidgets('Ensure red and green light widget exists',
(WidgetTester tester) async {
await _pumpWidget(tester);
expect(
find.byWidgetPredicate((widget) =>
widget is TrafficLightLampWidget &&
widget.color == Colors.red &&
widget.text == 'STOP' &&
!widget.openBarricade),
findsOneWidget,
);
expect(
find.byWidgetPredicate((widget) =>
widget is TrafficLightLampWidget &&
widget.color == Colors.green &&
widget.text == 'GO' &&
widget.openBarricade),
findsOneWidget,
);
});
By introducing a custom finder the test can be simplified to
testWidgets('Ensure red and green light widget exists',
(WidgetTester tester) async {
await _pumpWidget(tester);
expect(RedLightLampFinder(), findsOneWidget);
expect(GreenLightLampFinder(), findsOneWidget);
});
Notice how introducing a custom finder makes the code concise and clean.
When writing widget and integration tests, you pass an instance of a subclass of Finder
as the first argument to the expect()
function.
expect(find.text('title'), findsOneWidget);
The expect()
function accepts a Matcher
as the second parameter.
Matcher makes your test code clean and concise by abstracting validation code. Assume you have a class named TrafficLightController
that contains logic to control a traffic light.
class TrafficLightController{
final redLight = RedLight();
final greenLight = GreenLight();
void stop(){
redLight.activate();
greenLight.deactivate();
}
void go(){
greenLight.activate();
redLight.deactivate();
}
}
class RedLight extends TrafficLight {
@override
Color get lightColor => isActive ? Colors.red : Colors.grey;
@override
String? get trafficText => isActive ? 'Stop' : null;
@override
bool get isBarricadeClosed => isActive ? true : false;
}
You want to write tests to validate that the red and green light behaves correctly when activated or deactivated by invoking the stop()
or go()
function.
Without introducing a custom matcher, your test assertion logic for the above code will probably look like this.
test(
'Test traffic stop',
() {
final trafficController = TrafficLightController();
trafficController.stop();
expect(trafficController.redLight.lightColor, Colors.red);
expect(trafficController.redLight.trafficText, 'Stop');
expect(trafficController.redLight.isBarricadeClosed, true);
trafficController.go();
expect(trafficController.redLight.lightColor, Colors.grey);
expect(trafficController.redLight.trafficText, null);
expect(trafficController.redLight.isBarricadeClosed, false);
},
);
By introducing a custom matcher you hide (abstract) assertion logic. The test code above can then be refactored to the code below:
test(
'Test traffic stop',
() {
final trafficController = TrafficLightController();
trafficController.stop();
// Where ActiveRedLightMatcher is the name of a custom matcher
expect(trafficController.redLight, ActiveRedLightMatcher());
trafficController.go();
// Where InActiveRedLightMatcher is the name of a custom matcher
expect(trafficController.redLight, InactiveRedLightMatcher());
},
);
Given that custom finder and matcher aid in writing clean and concise test code, how does finder-matcher-gen help you?
This package aims to accomplish two things:
It does this by easing the pain of writing these custom matchers or finders from you as a developer through code generation.
For a quick start on using this package, go to the quickstart page. For a deeper understanding of generating finder and matcher classes, go to the generate finder and generate matcher pages.