25

I have a Widget with NetworkImage (so far with hard-coded url).
I would like to widget test this Widget, but I got 404 when I run widget test (url is 100% valid).
How can I make NetworkImages load themselves or (which would be better) ignore them so that my tests won't fail because of 404?

Marcin Szałek
  • 4,609
  • 5
  • 30
  • 43

5 Answers5

26

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.
    });
  });
}
Iiro Krankka
  • 4,959
  • 5
  • 38
  • 42
16

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),
  );
});
Hugo Passos
  • 7,719
  • 2
  • 35
  • 52
  • 1
    That one worked perfectly for me. I don't know how safe it is to just override stuff, but it's simpler than mocking everything. –  Oct 10 '20 at 14:31
16

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()));
RobDil
  • 4,036
  • 43
  • 52
  • This should totally be the newer accepted answer, since that package isn't maintained anymore. – venir May 16 '22 at 10:42
10

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.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 1
    I had placed `createHttpClient = createMockImageHttpClient` inside `setUpAll` and then worked like a charm! :) Thanks! :) – Marcin Szałek Mar 27 '18 at 15:29
  • 1
    I believe `createHttpClient` is deprecated in newer versions of flutter. See https://github.com/flutter/flutter/issues/15447. Do you know what the recommended way of achieving this is now? – Georg Grab Jun 22 '18 at 08:15
  • @MarcinSzałek, Could you show full code of your `setUpAll` function? – hunghd Jul 09 '18 at 10:16
  • @hunghd I added another example that uses the mockito package. – Günter Zöchbauer Jul 09 '18 at 10:55
  • @hunghd Sorry, but at this moment my code for this doesn't seem to work. Did you manage to use Gunter's one? @GünterZöchbauer Can you compile your code? It looks like `TestHttpOverrides` constructor expects `Uri` as keys in map, and you pass strings and gives me an error... – Marcin Szałek Jul 13 '18 at 18:03
  • Oerhaps I copied the wrong code line. Just use `Uri.parse('theUrl')` to get an Uri instead of string.(Not on my computer to check) – Günter Zöchbauer Jul 13 '18 at 18:34
  • Do I need to use a Base64 encoded image to make it pass in the console? I've tried both, loading from assets and encoded but: 1. from the assets, I just get an error that the file can't be loaded 2. encoded image, the test pass but only in with "flutter run ..." in console (the image is not shown, but it allows me to test the Widget without failing before) – ngbl Sep 24 '18 at 16:21
  • No, without Base64 you can omit `Uint8List get dummyImageData => BASE64.decode(dummyJpgImageBase64);` I don't know if loading from assets is supported. I have it as Base64 encoded string in Dart code. – Günter Zöchbauer Sep 24 '18 at 16:27
  • 1
    Since TestHttpOverrides() deprecated, used MockHttpOverrides() and it worked – SardorbekR Nov 17 '22 at 08:50
3

I updated the code from Günter Zöchbauer answer using null safety and the mocktail package.

image_mock_http_client.dart

import 'dart:io';

import 'package:mocktail/mocktail.dart';

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

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

  @override
  HttpClient createHttpClient(SecurityContext? context) {
    final client = MockHttpClient();
    final request = MockHttpClientRequest();
    final response = MockHttpClientResponse(data);
    final headers = MockHttpHeaders();

    /// Comment the exception when stub is missing from client
    /// because it complains about missing autoUncompress stub
    /// even setting it up as shown bellow.
    // throwOnMissingStub(client);
    throwOnMissingStub(request);
    throwOnMissingStub(response);
    throwOnMissingStub(headers);

    // This line is not necessary, it can be omitted.
    when(() => client.autoUncompress).thenReturn(true);

    // Use decompressed, otherwise you will get bad data.
    when(() => response.compressionState)
        .thenReturn(HttpClientResponseCompressionState.decompressed);

    // Capture the url and assigns it to requestedUrl from MockHttpClientResponse.
    when(() => client.getUrl(captureAny())).thenAnswer((invocation) {
      response.requestedUrl = invocation.positionalArguments[0] as Uri;
      return Future<HttpClientRequest>.value(request);
    });

    // This line is not necessary, it can be omitted.
    when(() => request.headers).thenAnswer((_) => headers);

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

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

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

    when(
      () => response.listen(
        captureAny(),
        cancelOnError: captureAny(named: 'cancelOnError'),
        onDone: captureAny(named: 'onDone'),
        onError: captureAny(named: '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 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;

  // It is not necessary to override this method to pass the test.
  @override
  Future<S> fold<S>(
    S initialValue,
    S Function(S previous, List<int> element) combine,
  ) {
    return Stream.fromIterable([data[requestedUrl]])
        .fold(initialValue, combine as S Function(S, List<int>?));
  }
}

class MockHttpHeaders extends Mock implements HttpHeaders {}

my_test.dart

const _imageUrl = 'https://your.image.uri.here';

void main() {
  setUp(() async {
    registerFallbackValue(Uri());

    // Load an image from assets and transform it from bytes to List<int>
    final _imageByteData = await rootBundle.load('assets/images/image.png');
    final _imageIntList = _imageByteData.buffer.asInt8List();

    final _requestsMap = {
      Uri.parse(_imageUrl): _imageIntList,
    };

    HttpOverrides.global = MockHttpOverrides(_requestsMap);
  });
  
  ...
}
Abel Rodríguez
  • 474
  • 5
  • 6