If you haven't read part one, you can start here:

Testing GoRouter in Flutter
Discover how to test two of the most common use cases of navigation with GoRouter!

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!