Running widget tests headlessly without any simulator or emulator is pretty dope. It’s quite mindblowing to run some automated taps, swipes, and flings and not need to stare that Waiting for emulator to start... message for minutes.

But then, with the tears of pure joy falling down your cheeks, you try to run a widget test that pumps an Image.network widget. And suddenly those tears are not tears of joy anymore.

A really dumbed-down version that illustrates the problem might look like this:

test/my_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('my image test', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Image.network('https://example.com/image.png'),
      ),
    );
    
    /// Crash!
  });
}

If you try to run the above test, it will crash. Any url you provide, no matter if it’s fake or real, will cause the widget to load the image url and fail instantly.

You’ll be greeted with a long error message that looks like this.

══╡ EXCEPTION CAUGHT BY IMAGE RESOURCE SERVICE ╞════════════════════════════════════════════════════
The following _Exception was thrown resolving an image codec:
Exception: HTTP request failed, statusCode: 400, https://example.com/image.png
...
When the exception was thrown, this was the stack:
#0      NetworkImage._loadAsync (package:flutter/src/painting/image_provider.dart:490:7)
<asynchronous suspension>
#1      NetworkImage.load (package:flutter/src/painting/image_provider.dart:469:14)
#2      ImageProvider.resolve.<anonymous closure>.<anonymous closure> (package:flutter/src/painting/image_provider.dart:266:86)
...
Test failed. See exception logs above.

This might seem weird and annoying at first, but it turns out that this is a quite sane default behavior to have.

#Why does this happen?

By default, all HTTP requests in widget tests will always return empty responses, with an HTTP status 400 - Bad Request. In other words - every HTTP request will raise an exception and fail your widget tests. And this is perfectly good default behavior in tests. But why?

Making real HTTP requests in tests is problematic. For one, it makes running tests slower. It also makes tests unpredictable and flaky. They could fail due to poor connectivity issues or server not responding, and those are something out of our control. We don’t want flaky tests depending on outside conditions.

It would be quite nice if there was some way to intercept those requests that the Image.network widget makes in widget tests. And there is.

#The solution

The solution is to replace the default HTTP client with one that always responds with a transparent image and an HTTP status of 200 - OK. There’s a sample on how to do this in the Flutter repo and in the widget tests of the flutter_markdown library, but it might get a little boring to copy and paste this thing around.

That’s why I created a little library that wraps this into a nice little package. There are only a few steps to start using it.

Step 1: include it in your dev_dependencies block:

pubspec.yaml
dev_dependencies:
  image_test_utils: ^1.0.0

Step 2: wrap your widget test with a provideMockedNetworkImages method:

test/my_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:image_test_utils/image_test_utils.dart';

void main() {
  testWidgets('my image test', (WidgetTester tester) async {
    provideMockedNetworkImages(() async {
      /// Now we can pump NetworkImages without crashing our tests. Yay!
      await tester.pumpWidget(
        MaterialApp(
          home: Image.network('https://example.com/image.png'),
        ),
      );
      
      /// No crashes.
    });
  });
}

(For a more comprehensive sample, see this widget test in the inKino app.)

Step 3: there’s no step three! Just see your widget tests with Image.network widgets pass with flying colors.

Behind the scenes, wrapping your code with provideMockedNetworkImages creates a new Zone, in which the default HTTP client has been replaced with a mocked one. The mocked client always responds with a transparent image and a 200 - OK status. And your Image.network widget tests are now happier than ever.

And yes - provideMockedNetworkImages will override all of your HTTP GET requests. But you generally don’t want to do any API communication in your tests anyway. And luckily anything other than Image.network is quite easy to mock out.