Flutter Widget Tests with NetworkImage

If you have this really unusual situation where the widget test is all about whether the images get correctly fetched, you can undo the override.

For every test:

setUpAll(() => HttpOverrides.global = null);

For a single test:

testWidgets('Image gets correctly fetched.', (tester) {
  HttpOverrides.runZoned(
    // Run your tests.
    () {},
    createHttpClient: (securityContext) => MockHttpClient(securityContext),
  );
});

I use

import 'package:flutter/services.dart' show createHttpClient;

final imageUri = Uri.parse('http://example.com$dummyImagePath');

testWidgets( ...) {
  createHttpClient = createMockImageHttpClient;

  await tester.pumpWidget(new TestWrapperWidget(
    child: (_) => new ImageWidget(name: text, url: imageUri)));

}
import 'dart:async' show Future;

import 'package:http/http.dart' show Client, Response;
import 'package:http/testing.dart' show MockClient;
import 'dummy_image_data.dart'
    show dummyImageData;

const String dummyImagePath = '/image.jpg';
Client createMockImageHttpClient() => new MockClient((request) {
      switch (request.url.path) {
        case dummyImagePath:
          return new Future<Response>.value(new Response.bytes(
              dummyImageData, 200,
              request: request, headers: {'Content-type': 'image/jpg'}));
        default:
          return new Future<Response>.value(new Response('', 404));
      }
    });
Uint8List get dummyImageData => BASE64.decode(dummyJpgImageBase64);    

(I created the image data Base64 using http://base64.wutils.com/encoding-online/)

const String dummyAvatarJpgImageBase64 =
'/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIi'
...   
'itf93F+MLRdehP4ZutvWj8m+rjzpz//Z';

This way the test also works when I start it with flutter run -t test/image_test.dart, but the image data can also be just served from an image file for normal test runs.

Using the mockito package

image_mock_http_client.dart

import 'dart:async' show Future, Stream;
import 'dart:io'
    show
        HttpClient,
        HttpClientRequest,
        HttpClientResponse,
        HttpHeaders,
        HttpOverrides,
        HttpStatus,
        SecurityContext;

import '.dummy_image_data.dart';
import 'package:mockito/mockito.dart'
    show Mock, any, anyNamed, captureAny, throwOnMissingStub, when;

const String dummyAvatarImagePath = '/avatar.jpg';

class TestHttpOverrides extends HttpOverrides {
  TestHttpOverrides(this.data);

  final Map<Uri, List<int>> data;

  @override
  HttpClient createHttpClient(SecurityContext context) =>
      createMockImageHttpClient(context, data);
}

// Returns a mock HTTP client that responds with an image to all requests.
MockHttpClient createMockImageHttpClient(
    SecurityContext _, Map<Uri, List<int>> data) {
  final client = new MockHttpClient();
  final request = new MockHttpClientRequest();
  final response = new MockHttpClientResponse(data);
  final headers = new MockHttpHeaders();

  throwOnMissingStub(client);
  throwOnMissingStub(request);
  throwOnMissingStub(response);
  throwOnMissingStub(headers);

  when<dynamic>(client.getUrl(captureAny)).thenAnswer((invocation) {
    response.requestedUrl = invocation.positionalArguments[0] as Uri;
    return new Future<HttpClientRequest>.value(request);
  });

  when(request.headers).thenAnswer((_) => headers);

  when(request.close())
      .thenAnswer((_) => new Future<HttpClientResponse>.value(response));

  when(response.contentLength)
      .thenAnswer((_) => data[response.requestedUrl].length);

  when(response.statusCode).thenReturn(HttpStatus.ok);

  when(
    response.listen(
      any,
      cancelOnError: anyNamed('cancelOnError'),
      onDone: anyNamed('onDone'),
      onError: anyNamed('onError'),
    ),
  ).thenAnswer((invocation) {
    final onData =
        invocation.positionalArguments[0] as void Function(List<int>);

    final onDone = invocation.namedArguments[#onDone] as void Function();

    final onError = invocation.namedArguments[#onError] as void Function(Object,
        [StackTrace]);

    final cancelOnError = invocation.namedArguments[#cancelOnError] as bool;

    return new Stream<List<int>>.fromIterable([data[response.requestedUrl]])
        .listen(onData,
            onDone: onDone, onError: onError, cancelOnError: cancelOnError);
  });
  return client;
}

class MockHttpClient extends Mock implements HttpClient {}

class MockHttpClientRequest extends Mock implements HttpClientRequest {}

class MockHttpClientResponse extends Mock implements HttpClientResponse {
  MockHttpClientResponse(this.data);
  final Map<Uri, List<int>> data;
  Uri requestedUrl;

  @override
  Future<S> fold<S>(S initialValue, S combine(S previous, List<int> element)) =>
      new Stream.fromIterable([data[requestedUrl]]).fold(initialValue, combine);
}

class MockHttpHeaders extends Mock implements HttpHeaders {}

my_test.dart

import 'image_mock_http_client.dart' show TestHttpOverrides;

...

  setUp(() async {
    HttpOverrides.global = new TestHttpOverrides({
      'http://example.com/my_image.png':               dummyAvatarImageData,
      'http://example.com/other_image.png: dummyPngImageData,
    });
  });

dummyAvatarImageData and dummyPngImageData are list<int> and contain the image data.


In widget tests, the default HTTP client has been replaced with one that always returns 400s. There's a sample on how to do this in the flutter_markdown repo along with couple other places. I used to copy and paste this to every project, but I did it enough times to get quite bored.

There's now a library for this (by me), called "image_test_utils". You can wrap your widget tests with a provideMockedNetworkImages method, which replaces the mocked HTTP client with one that always returns transparent images. Which in turn makes your tests pass.

pubspec.yaml:

dev_dependencies:
  image_test_utils: ^1.0.0

my_image_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.
    });
  });
}

A few years later and now that image_test_utils package seems no longer maintained, here is another easy solution to this problem.

I used the network_image_mock package (supports nullsafety) and added just two lines of code to my test. Wrap your pumpWidget call with mockNetworkImagesFor like this and you won't get the image loading errors anymore:

mockNetworkImagesFor(() => tester.pumpWidget(makeTestableWidget()));

Tags:

Dart

Flutter