Testing GoRouter in Flutter

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

Testing GoRouter in Flutter

When I started coding, I spent a long time not paying attention to testing. I was alone doing small projects, and testing seemed a waste of time.

That seems very far from what I'm doing today. I'm in a team of 2 to 5 people coding on the same project during my daily work. That means that documenting the code and testing it to make it more robust is essential.

One of the most complex parts has always been choosing which part to test. How to decide if this function that seems to be a simple passthrough deserves a test? Is the onPress callback complex enough to earn a test? So many questions that are hard to answer until you decide something

What if I start testing everything?

This series aims at helping you achieve the 100% coverage you don't know you need. To eliminate all the minor bugs and inconsistencies in your Flutter application.

GoRouter

GoRouter is a solution to handle Navigator 2.0 more easily in Flutter. It can help you create a reliable router responsible for handling which pages are shown to your user. But GoRouter logic can be quite hard to test. That's why I decided to dedicate this first article to it.

In this article, I'll cover two common cases for GoRouter that you'll be able to reproduce quickly:

  1. Is the right page of my app shown for a specific URL?
  2. Am I redirected to the right page when clicking on this button?

At the end of this article, you'll be able to test your navigation confidently!

Is the right page of my app shown for a specific URL?

As Flutter apps are getting more multiplatform than ever, deep linking is crucial to ensure that your users are redirected to the right page of your app from everywhere. You need to ensure that the proper page is displayed from any URL.

The first step is to ensure that you can inject your initial location into your Router. I often create a small function to be able to reuse my Router everywhere I might need it:

GoRouter router([String? initialLocation]) =>
    GoRouter(
      initialLocation: initialLocation ?? Routes.home,
      routes: [
        GoRoute(
          path: Routes.home,
          builder: (_, __) => const HomePage(),
        ),
        GoRoute(
          path: Routes.login,
          builder: (_, __) => const LoginPage(),
        )
      ],
    );

The route file is something simple like that (the Equatable part will be necessary for part 2):

import 'package:equatable/equatable.dart';

class Routes extends Equatable {
  static const home = '/';

  static const login = '/login';

  @override
  List<Object?> get props => [home, login];
}

I like to create an Extension on WidgetTester to make testing easy, and that's what I'm going to start doing:

extension PumpApp on WidgetTester {
  Future<void> pumpRealRouterApp(
    String location,
    Widget Function(Widget child) builder, {
    bool isConnected = true,
  }) {
    // Logic to initialize my StateManagement with the
    // value of isConnected
    // ...

    return pumpWidget(
      builder(
        MaterialApp.router(
          routeInformationParser:
              router(location).routeInformationParser,
          routerDelegate: router(location).routerDelegate,
        ),
      ),
    );
  }
}
pumpRealRouterApp

Then you have to run this code to check that your Router displays the correct page.

    testWidgets('renders LoginPage via Router', (tester) async {
      await tester.pumpRealRouterApp(
        Routes.login,
        (child) => child,
        isConnected: false,
      );
      expect(find.byType(LoginPage), findsOneWidget);
      expect(find.byType(BackButton), findsNothing);
    });

That's it! You have successfully tested that your Router displays the correct information when you start your app! It can be beneficial to test that your user doesn't see the LoginPage when he is already logged in!

Am I redirected to the right page when clicking on this button?

This second use case is a little bit more tricky. GoRouter doesn't offer a key or something that could give information about the current location. My first approach was to get a context in which GoRouter was initialized to get the GoRouter location. But this approach rapidly led to context not being outdated, so I tried something else.

In the first part, I added the props property to the Routes class to get the list of all the Routes. I will use it to create another extension:

  String getRouterKey(String route) {
    return 'key_$route';
  }
  
  Future<void> pumpRouterApp(Widget widget) {
    const initialLocation = '/_initial';

    final _router = GoRouter(
      initialLocation: initialLocation,
      routes: [
        GoRoute(
          path: initialLocation,
          builder: (context, state) => widget,
        ),
        ...Routes()
            .props
            .map(
              (e) => GoRoute(
                path: e! as String,
                builder: (context, state) => Container(
                  key: Key(
                    getRouterKey(e as String),
                  ),
                ),
              ),
            )
            .toList()
      ],
    );

    return pumpWidget(
      MaterialApp.router(
        routeInformationParser: _router.routeInformationParser,
        routerDelegate: _router.routerDelegate,
      ),
    );
  }
pumpRouterApp

You can see that I map all my routes to a Key in a simple Container. This allows me to check if the correct Container is displayed since it will correspond to the Route.

Imagine that I have a Button that is supposed to redirect to a new page. I can now write this simple code:

testWidgets('is redirected when button is tapped', (tester) async {
    await tester.pumpRouterApp(
      const HomePage(),
    );

    await tester.tap(find.byIcon(Icons.add));
    await tester.pumpAndSettle();
    expect(find.byKey(Key(getRouterKey(Routes.newPage))), findsOneWidget);
  });

With just this simple code, you are now sure that you have redirected your user to the correct page without caring about mocking all the potential dependencies of your next page! 🎉

Final thoughts

I hope you learn some things from this article. I spent time researching the best way to test GoRouter and would love to hear how you test your apps! ✌️ You can reach me on Twitter to ask me any questions.  

You can subscribe to my free newsletter below if you like this article! You will only receive my new articles, no spam.

You can check part two of this series here:

Testing GoRouter in Flutter #2
If you haven’t read part one, you can start here: Testing GoRouter in FlutterDiscover how to test two of the most common use cases of navigation with GoRouter!Guillaume BernosGuillaume BernosMy last blog post about testing GoRouter gained some visibility. So much so that @csells asked me if I could