【问题标题】:Flutter Widget Tests with NetworkImage使用 NetworkImage 进行 Flutter 小部件测试
【发布时间】:2018-08-16 09:36:41
【问题描述】:

我有一个 WidgetNetworkImage(到目前为止是硬编码的 url)。
我想对这个小部件进行小部件测试,但是当我运行小部件测试时得到 404(网址 100% 有效)。
我怎样才能让NetworkImages 自己加载或(哪个更好)忽略它们,这样我的测试就不会因为 404 而失败?

【问题讨论】:

    标签: dart flutter


    【解决方案1】:

    在小部件测试中,默认的 HTTP 客户端 has been replaced 总是返回 400s。在flutter_markdown repo 和其他几个地方有一个关于如何做到这一点的示例。我曾经将它复制并粘贴到每个项目中,但我做了足够多的时间来感到很无聊。

    现在有一个库(由我创建),名为 "image_test_utils"。您可以使用 provideMockedNetworkImages 方法包装您的小部件测试,该方法将模拟的 HTTP 客户端替换为始终返回透明图像的客户端。这反过来又使您的测试通过。

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

    【讨论】:

    • 看起来棒极了!我一定会试试这个!
    • 我已经实现了Network.ImageframeBuilder 并且它永远不会被正确调用,帧字段始终为空
    • 这个库不再维护。我建议不要使用它。
    • 请更新您的库以支持空安全
    【解决方案2】:

    如果您遇到这种非常不寻常的情况,即小部件测试完全是关于是否正确获取图像,您可以撤消覆盖。

    对于每个测试:

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

    对于单个测试:

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

    【讨论】:

    • 那个对我来说很完美。我不知道只覆盖东西有多安全,但它比模拟一切更简单。
    【解决方案3】:

    我用

    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);    
    

    (我使用http://base64.wutils.com/encoding-online/创建了图片数据Base64)

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

    这样,当我使用flutter run -t test/image_test.dart 启动它时,测试也可以工作,但图像数据也可以只从图像文件中提供,以进行正常的测试运行。

    使用mockito

    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,
        });
      });
    

    dummyAvatarImageDatadummyPngImageDatalist&lt;int&gt; 并包含图像数据。

    【讨论】:

    • 我已将createHttpClient = createMockImageHttpClient 放入setUpAll 中,然后像魅力一样工作! :) 谢谢! :)
    • 我相信 createHttpClient 在新版本的颤振中已被弃用。见github.com/flutter/flutter/issues/15447。你知道现在推荐的实现方式是什么吗?
    • @MarcinSzałek,你能显示你的setUpAll函数的完整代码吗?
    • @hunghd 我添加了另一个使用 mockito 包的示例。
    • @hunghd 抱歉,但此时我的代码似乎不起作用。你设法使用了甘特的那个吗? @GünterZöchbauer 你能编译你的代码吗?看起来 TestHttpOverrides 构造函数期望 Uri 作为映射中的键,而您传递字符串并给我一个错误...
    【解决方案4】:

    几年后,现在image_test_utils 包似乎不再维护,这是解决此问题的另一个简单方法。

    我使用了network_image_mock 包(支持nullsafety)并且只在我的测试中添加了两行代码。像这样用mockNetworkImagesFor 包裹你的pumpWidget 调用,你就不会再收到图像加载错误了:

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

    【讨论】:

      【解决方案5】:

      我使用空安全和mocktail 包更新了Günter Zöchbauer 答案中的代码。

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

      【讨论】:

        猜你喜欢
        • 2021-11-13
        • 2021-09-22
        • 2019-02-25
        • 2021-05-09
        • 2021-04-29
        • 2019-06-13
        • 2021-04-14
        • 2021-05-14
        • 2019-05-23
        相关资源
        最近更新 更多