If you haven't read part one, you can start here:
My last blog post about testing GoRouter gained some visibility. So much so that @csells asked me if I could improve the coverage of examples!
Even if I had tested GoRouter in my current project, testing someone else's code is always trickier. In this case, examples needed to stay ultra-concise to prevent complexifying code for newcomers.
Even if my approach worked, the code needed some changes that I felt shouldn't be necessary, especially for unit testing. I felt like I could do better! ✌️
As @robsonsilv4 pointed out, VGVentures? created a library designed to answer this kind of problem for Navigator
.
It allows the developer to use the familiar testing syntax:
verify(
() => navigator.push(any(that: isRoute<void>(whereName: equals('/settings')))),
).called(1);
Let's create something similar for GoRouter!
MockGoRouterProvider
The first step to being able to verify if a context.go
has been called is to set up a mock version of it:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router/src/inherited_go_router.dart';
import 'package:mocktail/mocktail.dart';
class MockGoRouter extends Mock implements GoRouter {}
class MockGoRouterProvider extends StatelessWidget {
const MockGoRouterProvider({
required this.goRouter,
required this.child,
Key? key,
}) : super(key: key);
/// The mock navigator used to mock navigation calls.
final MockGoRouter goRouter;
/// The child [Widget] to render.
final Widget child;
@override
Widget build(BuildContext context) => InheritedGoRouter(
goRouter: goRouter,
child: child,
);
}
The code is concise, but It will come in handy fast.
A sample test
Let's imagine that we have this widget (taken from one of the GoRouter examples)
class HomeScreen extends StatelessWidget {
const HomeScreen({required this.families, Key? key}) : super(key: key);
final List<Family> families;
@override
Widget build(BuildContext context) {
final info = context.read<LoginInfo>();
return Scaffold(
appBar: AppBar(
title: const Text(App.title),
actions: [
IconButton(
onPressed: info.logout,
tooltip: 'Logout: ${info.userName}',
icon: const Icon(Icons.logout),
)
],
),
body: ListView(
children: [
for (final f in families)
ListTile(
title: Text(f.name),
onTap: () => context.go('/family/${f.id}'),
)
],
),
);
}
}
We want to test if when we click on a ListTile
. , we are correctly triggering the context.go
.
testWidgets('should redirect to family when clicking on tile',
(tester) async {
loginInfo.login('Username');
final mockGoRouter = MockGoRouter();
await tester.pumpWidget(
MaterialApp(
home: MockGoRouterProvider(
goRouter: mockGoRouter,
child: ChangeNotifierProvider.value(
value: loginInfo,
child: HomeScreen(families: Families.data),
),
),
),
);
await tester.tap(find.byType(ListTile).first);
await tester.pumpAndSettle();
verify(() => mockGoRouter.go('/family/f1')).called(1);
verifyNever(() => mockGoRouter.go('/family/f2'));
});
This test will verify that we are only calling context.go('/family/f1')
.
In conjunction with the other test presented in part one, you can now test all kinds of GoRouter scenarios.
Wrap up
If you have any more ideas of GoRouter code that you cannot test with those methods, let me know on Twitter! I'll be happy to help. I'll probably post a PR with the MockGoRouterProvider
class and some examples tested for GoRouter, but at least you can already use it!