# Writing widget tests for navigation events

Source: https://iiro.dev/writing-widget-tests-for-navigation-events/
Published: 2018-08-22

Learn how to test push and pop navigation events in Flutter with WidgetTester APIs and mockito.

---

I love automated testing.

When done correctly, it saves time spent on bug hunting and helps you avoid nasty regressions. If done before any manual testing, it saves time implementing features too. That all being said, navigation tests are not going to be first on the list of important things to test. However, a question about testing navigation came up in the Slack group. As someone who did a couple of regression tests for navigation before, I figured out it's worth to share.

Our sample app consists of two pages: a `MainPage` and a `DetailsPage`. From the main page, the user can click a button, and he ends up in the details page. The details page receives a friendly greeting in its constructor parameter. 


**File:** `lib/main_page.dart`


```dart
import 'details_page.dart';

class MainPage extends StatelessWidget {
  void _navigateToDetailsPage(BuildContext context) {
    final route = MaterialPageRoute(builder: (_) => DetailsPage('Hello!'));
    Navigator.of(context).push(route);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Testing navigation'),
      ),
      body: RaisedButton(
        onPressed: () => _navigateToDetailsPage(context),
        child: Text('Navigate to details page!'),
      ),
    );
  }
}
```


The details page is a `StatelessWidget` displaying a simple centered text. It takes in the message from the main page and then displays it in a `Text` widget.


**File:** `lib/details_page.dart`


```dart
class DetailsPage extends StatelessWidget {
  DetailsPage(this.message);
  final String message;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Details page'),
      ),
      body: Center(
        child: Text(message),
      ),
    );
  }
}
```


Quite something, isn't it? Our app is going to be the next Facebook. You wait and see.

## How do we test this thing?

To make things easier, we'll use a `Key` to get the reference to the _"Navigate to details page!"_ button. And yes, we could find the button by its text without using any keys. However, I generally prefer using keys to make things more foolproof. 

If the app is localized in different languages, finding things by their text contents could become quite unpredictable. For example, _"Navigate to details page"_ becomes _"Avaa tietosivu"_ in Finnish. Now we would have to lock our test app to a single language and cross our fingers that nobody messes up the translation files. And since that special someone will be ourselves doing yet another late Friday production release, crossing our fingers won't help.

Since we're now convinced that keys are the way to go, we'll define a new key and assign it to the button by passing it as the `key` constructor parameter.


**File:** `lib/main_page.dart`


```dart
class MainPage extends StatelessWidget {
  // 1: Define the key here...
  static const navigateToDetailsButtonKey = Key('navigateToDetails');

  // ...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // ...
      body: RaisedButton(
        // 2: ...and assign it to the RaisedButton here.
        key: navigateToDetailsButtonKey,
        // ...
      ),
    );
  }
}
```


Keys are not special to only `RaisedButtons` - pretty much all the widgets in Flutter have a key parameter in their constructor. And I say "pretty much all" only because I think every widget does but I'm not 100% sure. It's getting late, and I don't have the time go through all of them.

With `navigateToDetailsButtonKey` available as a _static_ field, our test cases can now easily identify the right thing to tap on the screen. To observe navigator events and verify they happen correctly, we use the [mockito package](https://pub.dartlang.org/packages/mockito) in conjunction with a [NavigatorObserver](https://docs.flutter.io/flutter/widgets/NavigatorObserver-class.html). In short, we'll pass a mocked navigation observer to our `MaterialApp` to verify various navigation events.


**File:** `test/navigation_test.dart`


```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

import 'main_page.dart';

class MockNavigatorObserver extends Mock implements NavigatorObserver {}

void main() {
  group('MainPage navigation tests', () {
    late NavigatorObserver mockObserver;

    setUp(() {
      mockObserver = MockNavigatorObserver();
    });

    Future<void> _buildMainPage(WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(
        home: MainPage(),

        // This mocked observer will now receive all navigation events
        // that happen in our app.
        navigatorObservers: [mockObserver],
      ));

      // The tester.pumpWidget() call above just built our app widget
      // and triggered the pushObserver method on the mockObserver once.
      verify(mockObserver.didPush(any!, any));
    }

    Future<void> _navigateToDetailsPage(WidgetTester tester) async {
      // Tap the button which should navigate to the details page.
      //
      // By calling tester.pumpAndSettle(), we ensure that all animations
      // have completed before we continue further.
      await tester.tap(find.byKey(MainPage.navigateToDetailsButtonKey));
      await tester.pumpAndSettle();
    }

    testWidgets(
        'when tapping "navigate to details" button, should navigate to details page',
        (WidgetTester tester) async {
      // TODO: Write the test case here.
    });
  });
}
```


A good convention is to name the mocks with a `Mock` prefix. Since we're mocking a `NavigatorObserver` class, we call our mock version of that `MockNavigatorObserver`.

## Testing pushed Routes

There are three simple steps for testing that the details page route is pushed.

1. Build the `MainPage` so that it's available for testing.
2. Find and tap the button.
3. Verify that we're now in the details page.

Since we created utility methods for building the page and tapping button previously, our test code becomes easy to follow.


**File:** `test/navigation_test.dart`


```dart
void main() {
  group('MainPage navigation tests', () {
    // ...
    testWidgets(
        'when tapping "navigate to details" button, should navigate to details page',
        (WidgetTester tester) async {
      await _buildMainPage(tester);
      await _navigateToDetailsPage(tester);

      // By tapping the button, we should've now navigated to the details
      // page. The didPush() method should've been called...
      verify(mockObserver.didPush(any!, any));

      // ...and there should be a DetailsPage present in the widget tree...
      expect(find.byType(DetailsPage), findsOneWidget);

      // ...with the message we sent from main page.
      expect(find.text('Hello!'), findsOneWidget);
    });
  });
}
```


Now our test code verifies that by pressing the button, a new route is pushed and that a `DetailsPage` exists in the widget tree. We could get rid of the `mockObserver` and that `verify` call altogether, but that would make our test less deterministic. 

Without the navigation observer, our test would still verify that after pressing the button a `DetailsPage` exists in the widget tree, but it wouldn't make sure that a new route was pushed. Pushing the button might replace the `body` of the main `Scaffold` instead of pushing a new route. The test would pass, but the behavior wouldn't be what we want.

### Sidenote: stricter finders

It's worth noting here that the `find.text('Hello!')` expectation could be more specific. To have a stricter expectation, we _could_ do this:


**File:** `test/navigation_test.dart`


```dart
// ...
var detailsFinder = find.byType(DetailsPage);
expect(detailsFinder, findsOneWidget);

var messageFinder = find.text('Hello!');
var strictMatch = find.descendant(of: detailsFinder, matching: messageFinder);
expect(strictMatch, findsOneWidget);
```


We still use the same finder for the `Hello!` message, but now we have another finder in place. By using `find.descendant`, we make sure that the text `Hello` belongs to a `DetailsPage` widget. In this case, I think it it's not needed though.


## Testing popped Routes with results

In the previous example, our details page was quite simple - it only contained a text widget. However, what if the details page passes some result when popping itself? In a real world, such a use case would be a login screen that pops with a result once the login process is done. In our app, we'll return something simple and static.


**File:** `lib/details_page.dart`


```dart
class DetailsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // ...
      body: RaisedButton(
        onPressed: () => Navigator.pop(context, 'I\'m a pirate.'),
        child: Text('Click me!'),
      ),
    );
  }
}
```


By clicking the button, we call `Navigator.pop` which closes the details page. Since we also pass a `result` parameter to the `pop` method, whoever invoked `Navigator.push()` on the details route will now receive the result. In this case, the main page receives a result of _"I'm a pirate."_.

To test this, we'll first want to create a `Key` for the button, which we'll call `popWithResultButtonKey`.


**File:** `lib/details_page.dart`


```dart
class DetailsPage extends StatelessWidget {
  // 1: Define the key here...
  static const popWithResultButtonKey = Key('popWithResult');

  // ...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // ...
      body: RaisedButton(
        // 2: ...and assign it to the RaisedButton here.
        key: popWithResultButtonKey,
        // ...
      ),
    );
  }
}
```


Now we can find the button by its `Key` and start writing a pop test.

To get a reference to the newly pushed route, we _capture_ it in our `verify` method call by using the `captureAny` matcher from mockito. This way, we can call `.captured.single` after the `verify` method to get a reference to the newly pushed route. Then we'll assign that to a variable that we'll call `pushedRoute`. 

```dart
final Route? pushedRoute =
    verify(mockObserver.didPush(captureAny!, any))
        .captured
        .single;
```


Now that we have a reference to that route, we'll also want to find out the pop result when the route gets popped. Upon inspecting the documentation by performing a `Cmd+Click` on the `Route` class in our IDE, we can see that a route has a getter called `popped`.


```dart
/// A future that completes when this route is popped off the navigator.
///
/// The future completes with the value given to [Navigator.pop], if any.
Future<T> get popped => _popCompleter.future.then((value) => value as T);
```


This fits perfectly for our use case. Since the return type is a `Future`, we can call `.then()` on it. The callback will be invoked once the future is complete. We can declare an empty variable and populate that with the pop result once the future resolves. 

<small><i>(In case you're wondering, we can't `await` a `Future` from the Flutter framework in tests. The future [will never resolve](https://github.com/flutter/flutter/issues/5728) and we'll get a time out exception.)</i></small>

After setting that up, we `tap` on the button on the `DetailsPage`, call `tester.pumpAndSettle()` to make sure that the route pops completely, and then `expect` the pop result to be `I'm a pirate.`.


**File:** `test/navigation_test.dart`


```dart
void main() {
  group('MainPage navigation tests', () {
    // ...
    testWidgets('tapping "click me" should pop with a result',
        (WidgetTester tester) async {
      // We'll build the main page and navigate to details first.
      await _buildMainPage(tester);
      await _navigateToDetailsPage(tester);

      // Then we'll verify that the details route was pushed again, but
      // this time, we'll capture the pushed route.
      final Route pushedRoute =
          verify(mockObserver.didPush(captureAny!, any)).captured.single;

      // We declare a popResult variable and assign the result to it
      // when the details route is popped.
      String? popResult;
      pushedRoute.popped.then((result) => popResult = result);

      // Pop the details route with a result by tapping the button.
      await tester.tap(find.byKey(DetailsPage.popWithResultButtonKey));
      await tester.pumpAndSettle();

      // popResult should now contain whatever the details page sent when
      // calling `Navigator.pop()`. In this case, "I'm a pirate".
      expect(popResult, 'I\'m a pirate.');
    });
  });
}
```


And that's it - now we've tested both pushing and popping routes with extra data sent to both directions.

## How to test API-dependent Routes?

At this point, you might say that in the real world things are never this simple. And you're right. Testing navigation in real apps that make network requests is a topic of its own.

In an ideal world, you don't want to deal with any (even mocked) API clients at all. In this case, `EventsPage` is just a dumb `StatelessWidget` that takes in a _view model_. That view model contains all the information for the `EventsPage` to know how to render itself. If something needs to be updated, a new view model is built and then passed to the `EventsPage`, causing it to update itself.

Here's a sample test case from my [Redux-based app, inKino](https://github.com/roughike/inKino):


**File:** `inKino tests: events_page_test.dart`


```dart
class MockEventsPageViewModel extends Mock implements EventsPageViewModel {}

class MockNavigatorObserver extends Mock implements NavigatorObserver {}

void main() {
  group('EventsPage tests', () {
    late MockNavigatorObserver observer;
    EventsPageViewModel? mockViewModel;

    setUp(() {
      observer = MockNavigatorObserver();
      mockViewModel = MockEventsPageViewModel();
    });

    Future<void> _buildEventsPage(WidgetTester tester) {
      return tester.pumpWidget(MaterialApp(
        home: EventsPage(mockViewModel),
        navigatorObservers: <NavigatorObserver>[observer],
      ));
    }

    testWidgets(
      'when tapping on an event poster, should navigate to event details',
      (WidgetTester tester) async {
        final List<Event> events = [Event(title: 'Test Title')];
        when(mockViewModel!.status).thenReturn(LoadingStatus.success);
        when(mockViewModel!.events).thenReturn(events);

        await _buildEventsPage(tester);

        // Building the events page should trigger the navigator observer
        // once.
        verify(observer.didPush(any!, any));

        await tester.tap(find.text('Test Title'));
        await tester.pumpAndSettle();

        verify(observer.didPush(any!, any));
        expect(find.byType(EventDetailsPage), findsOneWidget);
      },
    );
  });
}
```


As we can see, stateless widgets are quite easy to test. 

We can pass a view model in any needed state and verify whatever we need. In this example, I didn't verify that the details page contains anything - if it's present in the widget tree, I'm all good. However, I could test that there's now some additional text present by calling `expect(find.text('Some details'), findsOneWidget)` after pushing the details page.

To see some real-world navigation and lots of other tests, go and check out my [inKino app on GitHub](https://github.com/roughike/inKino). I use it as a testing reference all the time.

### The end.

I left out some code from the article for clarity. To get the full picture, see the [complete sample app right here](https://github.com/FlutterRocks/testing-navigation).
