【发布时间】:2018-08-16 09:36:41
【问题描述】:
我有一个 Widget 和 NetworkImage(到目前为止是硬编码的 url)。
我想对这个小部件进行小部件测试,但是当我运行小部件测试时得到 404(网址 100% 有效)。
我怎样才能让NetworkImages 自己加载或(哪个更好)忽略它们,这样我的测试就不会因为 404 而失败?
【问题讨论】:
我有一个 Widget 和 NetworkImage(到目前为止是硬编码的 url)。
我想对这个小部件进行小部件测试,但是当我运行小部件测试时得到 404(网址 100% 有效)。
我怎样才能让NetworkImages 自己加载或(哪个更好)忽略它们,这样我的测试就不会因为 404 而失败?
【问题讨论】:
在小部件测试中,默认的 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.Image 的frameBuilder 并且它永远不会被正确调用,帧字段始终为空
如果您遇到这种非常不寻常的情况,即小部件测试完全是关于是否正确获取图像,您可以撤消覆盖。
对于每个测试:
setUpAll(() => HttpOverrides.global = null);
对于单个测试:
testWidgets('Image gets correctly fetched.', () {
HttpOverrides.runZoned(
// Run your tests.
() {},
createHttpClient: (securityContext) => MockHttpClient(securityContext),
);
});
【讨论】:
我用
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,
});
});
dummyAvatarImageData 和 dummyPngImageData 是 list<int> 并包含图像数据。
【讨论】:
createHttpClient = createMockImageHttpClient 放入setUpAll 中,然后像魅力一样工作! :) 谢谢! :)
createHttpClient 在新版本的颤振中已被弃用。见github.com/flutter/flutter/issues/15447。你知道现在推荐的实现方式是什么吗?
setUpAll函数的完整代码吗?
TestHttpOverrides 构造函数期望 Uri 作为映射中的键,而您传递字符串并给我一个错误...
几年后,现在image_test_utils 包似乎不再维护,这是解决此问题的另一个简单方法。
我使用了network_image_mock 包(支持nullsafety)并且只在我的测试中添加了两行代码。像这样用mockNetworkImagesFor 包裹你的pumpWidget 调用,你就不会再收到图像加载错误了:
mockNetworkImagesFor(() => tester.pumpWidget(makeTestableWidget()));
【讨论】:
我使用空安全和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);
});
...
}
【讨论】: