Mocking internet offline in Flutter integration tests with Dio and Riverpod

Jack Allison
3 min readJul 25, 2023

--

Sometimes, users lose access to the internet. While the users themselves may have a breakdown and scream at their phones until normal service resumes, your app should handle these situations in a more robust manner. Key to ensuring that these situations are handled appropriately is testing, specifically integration testing. This can be surprisingly hard to handle and there’s currently no easy out-of-the-box solution. Especially, if we want to get funky and have the ability to turn the internet on or off during tests. What follows is my solution to this problem using Riverpod and Dio.

There are 2 key parts to making this solution work

  1. A Riverpod StateProvider which exposes whether the internet should currently be mocked as offline.
  2. A Dio Interceptor which handles the mocking.

StateProvider to set whether it should mock

First, we need to create a very simple StateProvider.

final shouldMockInternetOfflineProvider = StateProvider((ref) => ShouldMockInternetOfflineNotifier(false));

class ShouldMockInternetOfflineNotifier extends StateNotifier<bool> {
ShouldMockInternetOfflineNotifier(super.state);

void updateState(bool value) {
state = value;
}

bool get shouldMockOffline => state;
}

All this does is store and allow us to update a boolean value representing whether the application should pretend that requests are failing.

In the production code this will never be updated and will remain false.

Interceptor to do the mocking

Now that we can set when to mock, we need to handle how we mock. To do this, we create a a class which extends Dio’s Interceptor class.

class MockInternetOfflineInterceptor extends Interceptor {
final ProviderRef ref;

MockInternetOfflineInterceptor({required this.ref});

@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
if (ref.read(shouldMockInternetOfflineProvider).shouldMockOffline) {
final dioException = DioException(requestOptions: options, type: DioExceptionType.connectionError, error: Exception('Mocking internet offline'));
handler.reject(dioException, true);
} else {
handler.next(options); // very important or else no requests will complete
}
}
}

We add this interceptor wherever we instantiate dio like so:

final dio = Dio();
final mockInternetOfflineInterceptor = MockInternetOfflineInterceptor(ref: ref);
dio.interceptors.add(mockInternetOfflineInterceptor);

This means that every request which is sent using our dio object is intercepted by this method.

  • If the value in our provider is set to false, it will continue on as if the function was never called.
  • If it’s set to true, it will create a DioException with connectionError type and reject the request. This will return to your request-calling code in the same way that a real connection timeout error would return. Any interceptors which are set to run after this one will not be run.

Using it in your integration tests

It’s very simple to put these pieces together in your integration tests. Below is an example integration test which logs in to a app while online, tests making a request while offline and then tries it again when back online.

testWidgets('User is notified when they make a request which fails due to internet connectivity', (WidgetTester tester) async {
// Create our own ShouldMockInternetOfflineNotifier
final shouldMockInternetOfflineNotifier = ShouldMockInternetOfflineNotifier(false);

// Override the shouldMockInternetOfflineProvider to return our notifier
await tester.pumpWidget(
ProviderScope(
child: const MyApp(),
overrides: [shouldMockInternetOfflineProvider.overrideWith((ref) => shouldMockInternetOfflineNotifier)],
),
);

// Login while online
await tester.login();

// "Turn the internet off"
shouldMockInternetOfflineNotifier.updateState(true);

// "Make request" and find an appropriate error
await tester.makeRequest();
expect(connectionErrorFinder, findsOneWidget);

// "Turn the internet back on"
shouldMockInternetOfflineNotifier.updateState(false);

// "Make request" and find success screen
await tester.makeRequest();
expect(successScreenFinder, findsOneWidget);
});

Conclusion

As you can see, each part of this process is quite simple and leads to a easy-to-use API in your integration tests.

It’s possible to extend this concept further to allow for things like returning different DioException types like sendTimeout but I have purposefully only included the most basic way to get this kind of set-up working.

It is unfortunate that we need to make changes in the production code to allow for this rather than some kind of integration_test built-in method like turnOffInternet but alas, this will have to do for now.

--

--