Skip to content

Commit f28eb28

Browse files
authored
[flutter_test] Adds method to mock EventChannels (#123726)
[flutter_test] Adds method to mock EventChannels
1 parent 4e5e9f4 commit f28eb28

File tree

5 files changed

+216
-45
lines changed

5 files changed

+216
-45
lines changed

packages/flutter/test/services/platform_channel_test.dart

Lines changed: 26 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -265,69 +265,50 @@ void main() {
265265
});
266266

267267
group('EventChannel', () {
268-
const MessageCodec<dynamic> jsonMessage = JSONMessageCodec();
269268
const MethodCodec jsonMethod = JSONMethodCodec();
270269
const EventChannel channel = EventChannel('ch', jsonMethod);
271-
void emitEvent(ByteData? event) {
272-
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
273-
'ch',
274-
event,
275-
(ByteData? reply) {},
276-
);
277-
}
270+
278271
test('can receive event stream', () async {
279272
bool canceled = false;
280-
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMessageHandler(
281-
'ch',
282-
(ByteData? message) async {
283-
final Map<dynamic, dynamic> methodCall = jsonMessage.decodeMessage(message) as Map<dynamic, dynamic>;
284-
if (methodCall['method'] == 'listen') {
285-
final String argument = methodCall['args'] as String;
286-
emitEvent(jsonMethod.encodeSuccessEnvelope('${argument}1'));
287-
emitEvent(jsonMethod.encodeSuccessEnvelope('${argument}2'));
288-
emitEvent(null);
289-
return jsonMethod.encodeSuccessEnvelope(null);
290-
} else if (methodCall['method'] == 'cancel') {
273+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockStreamHandler(
274+
channel,
275+
MockStreamHandler.inline(
276+
onListen: (Object? arguments, MockStreamHandlerEventSink events) {
277+
events.success('${arguments}1');
278+
events.success('${arguments}2');
279+
events.endOfStream();
280+
},
281+
onCancel: (Object? arguments) {
291282
canceled = true;
292-
return jsonMethod.encodeSuccessEnvelope(null);
293-
} else {
294-
fail('Expected listen or cancel');
295-
}
296-
},
283+
},
284+
),
297285
);
298-
final List<dynamic> events = await channel.receiveBroadcastStream('hello').toList();
286+
final List<Object?> events = await channel.receiveBroadcastStream('hello').toList();
299287
expect(events, orderedEquals(<String>['hello1', 'hello2']));
300288
await Future<void>.delayed(Duration.zero);
301289
expect(canceled, isTrue);
302290
});
303291

304292
test('can receive error event', () async {
305-
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMessageHandler(
306-
'ch',
307-
(ByteData? message) async {
308-
final Map<dynamic, dynamic> methodCall = jsonMessage.decodeMessage(message) as Map<dynamic, dynamic>;
309-
if (methodCall['method'] == 'listen') {
310-
final String argument = methodCall['args'] as String;
311-
emitEvent(jsonMethod.encodeErrorEnvelope(code: '404', message: 'Not Found.', details: argument));
312-
return jsonMethod.encodeSuccessEnvelope(null);
313-
} else if (methodCall['method'] == 'cancel') {
314-
return jsonMethod.encodeSuccessEnvelope(null);
315-
} else {
316-
fail('Expected listen or cancel');
317-
}
318-
},
293+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockStreamHandler(
294+
channel,
295+
MockStreamHandler.inline(
296+
onListen: (Object? arguments, MockStreamHandlerEventSink events) {
297+
events.error(code: '404', message: 'Not Found.', details: arguments);
298+
},
299+
),
319300
);
320-
final List<dynamic> events = <dynamic>[];
321-
final List<dynamic> errors = <dynamic>[];
301+
final List<Object?> events = <Object?>[];
302+
final List<Object?> errors = <Object?>[];
322303
channel.receiveBroadcastStream('hello').listen(events.add, onError: errors.add);
323304
await Future<void>.delayed(Duration.zero);
324305
expect(events, isEmpty);
325306
expect(errors, hasLength(1));
326307
expect(errors[0], isA<PlatformException>());
327-
final PlatformException error = errors[0] as PlatformException;
328-
expect(error.code, '404');
329-
expect(error.message, 'Not Found.');
330-
expect(error.details, 'hello');
308+
final PlatformException? error = errors[0] as PlatformException?;
309+
expect(error?.code, '404');
310+
expect(error?.message, 'Not Found.');
311+
expect(error?.details, 'hello');
331312
});
332313
});
333314
}

packages/flutter_test/lib/flutter_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export 'src/frame_timing_summarizer.dart';
6969
export 'src/goldens.dart';
7070
export 'src/image.dart';
7171
export 'src/matchers.dart';
72+
export 'src/mock_event_channel.dart';
7273
export 'src/nonconst.dart';
7374
export 'src/platform.dart';
7475
export 'src/restoration.dart';
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
7+
import 'package:flutter/services.dart';
8+
9+
/// A mock stream handler for an [EventChannel] that mimics the native
10+
/// StreamHandler API.
11+
///
12+
/// The [onListen] callback is provided a [MockStreamHandlerEventSink] with
13+
/// the following API:
14+
/// - [MockStreamHandlerEventSink.success] sends a success event.
15+
/// - [MockStreamHandlerEventSink.error] sends an error event.
16+
/// - [MockStreamHandlerEventSink.endOfStream] sends an end of stream event.
17+
abstract class MockStreamHandler {
18+
/// Create a new [MockStreamHandler].
19+
MockStreamHandler();
20+
21+
/// Create a new inline [MockStreamHandler] with the given [onListen] and
22+
/// [onCancel] handlers.
23+
factory MockStreamHandler.inline({
24+
required MockStreamHandlerOnListenCallback onListen,
25+
MockStreamHandlerOnCancelCallback? onCancel,
26+
}) => _InlineMockStreamHandler(onListen: onListen, onCancel: onCancel);
27+
28+
/// Handler for the listen event.
29+
void onListen(Object? arguments, MockStreamHandlerEventSink events);
30+
31+
/// Handler for the cancel event.
32+
void onCancel(Object? arguments);
33+
}
34+
35+
/// Typedef for the inline onListen callback.
36+
typedef MockStreamHandlerOnListenCallback = void Function(Object? arguments, MockStreamHandlerEventSink events);
37+
38+
/// Typedef for the inline onCancel callback.
39+
typedef MockStreamHandlerOnCancelCallback = void Function(Object? arguments);
40+
41+
class _InlineMockStreamHandler extends MockStreamHandler {
42+
_InlineMockStreamHandler({
43+
required MockStreamHandlerOnListenCallback onListen,
44+
MockStreamHandlerOnCancelCallback? onCancel,
45+
}) : _onListenInline = onListen,
46+
_onCancelInline = onCancel;
47+
48+
final MockStreamHandlerOnListenCallback _onListenInline;
49+
final MockStreamHandlerOnCancelCallback? _onCancelInline;
50+
51+
@override
52+
void onListen(Object? arguments, MockStreamHandlerEventSink events) => _onListenInline(arguments, events);
53+
54+
@override
55+
void onCancel(Object? arguments) => _onCancelInline?.call(arguments);
56+
}
57+
58+
/// A mock event sink for a [MockStreamHandler] that mimics the native
59+
/// [EventSink](https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.EventSink.html)
60+
/// API.
61+
class MockStreamHandlerEventSink {
62+
/// Create a new [MockStreamHandlerEventSink] with the given [sink].
63+
MockStreamHandlerEventSink(EventSink<Object?> sink) : _sink = sink;
64+
65+
final EventSink<Object?> _sink;
66+
67+
/// Send a success event.
68+
void success(Object? event) => _sink.add(event);
69+
70+
/// Send an error event.
71+
void error({
72+
required String code,
73+
String? message,
74+
Object? details,
75+
}) => _sink.addError(PlatformException(code: code, message: message, details: details));
76+
77+
/// Send an end of stream event.
78+
void endOfStream() => _sink.close();
79+
}

packages/flutter_test/lib/src/test_default_binary_messenger.dart

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import 'dart:ui' as ui;
88
import 'package:fake_async/fake_async.dart';
99
import 'package:flutter/services.dart';
1010

11+
import 'mock_event_channel.dart';
12+
import 'widget_tester.dart';
13+
1114
/// A function which takes the name of the method channel, it's handler,
1215
/// platform message and asynchronously returns an encoded response.
1316
typedef AllMessagesHandler = Future<ByteData?>? Function(
@@ -197,6 +200,9 @@ class TestDefaultBinaryMessenger extends BinaryMessenger {
197200
///
198201
/// * [setMockMethodCallHandler], which wraps this method but decodes
199202
/// the messages using a [MethodCodec].
203+
///
204+
/// * [setMockStreamHandler], which wraps [setMockMethodCallHandler] to
205+
/// handle [EventChannel] messages.
200206
void setMockMessageHandler(String channel, MessageHandler? handler, [ Object? identity ]) {
201207
if (handler == null) {
202208
_outboundHandlers.remove(channel);
@@ -237,6 +243,9 @@ class TestDefaultBinaryMessenger extends BinaryMessenger {
237243
///
238244
/// * [setMockMethodCallHandler], which is similar but decodes
239245
/// the messages using a [MethodCodec].
246+
///
247+
/// * [setMockStreamHandler], which wraps [setMockMethodCallHandler] to
248+
/// handle [EventChannel] messages.
240249
void setMockDecodedMessageHandler<T>(BasicMessageChannel<T> channel, Future<T> Function(T? message)? handler) {
241250
if (handler == null) {
242251
setMockMessageHandler(channel.name, null);
@@ -302,6 +311,81 @@ class TestDefaultBinaryMessenger extends BinaryMessenger {
302311
}, handler);
303312
}
304313

314+
/// Set a handler for intercepting stream events sent to the
315+
/// platform on the given channel.
316+
///
317+
/// Intercepted method calls are not forwarded to the platform.
318+
///
319+
/// The given handler will replace the currently registered
320+
/// handler for that channel, if any. To stop intercepting messages
321+
/// at all, pass null as the handler.
322+
///
323+
/// Events are decoded using the codec of the channel.
324+
///
325+
/// The handler's stream messages are used as a response, after encoding
326+
/// them using the channel's codec.
327+
///
328+
/// To send an error, pass the error information to the handler's event sink.
329+
///
330+
/// {@macro flutter.flutter_test.TestDefaultBinaryMessenger.handlePlatformMessage.asyncHandlers}
331+
///
332+
/// Registered handlers are cleared after each test.
333+
///
334+
/// See also:
335+
///
336+
/// * [setMockMethodCallHandler], which is the similar method for
337+
/// [MethodChannel].
338+
///
339+
/// * [setMockMessageHandler], which is similar but provides raw
340+
/// access to the underlying bytes.
341+
///
342+
/// * [setMockDecodedMessageHandler], which is similar but decodes
343+
/// the messages using a [MessageCodec].
344+
void setMockStreamHandler(EventChannel channel, MockStreamHandler? handler) {
345+
if (handler == null) {
346+
setMockMessageHandler(channel.name, null);
347+
return;
348+
}
349+
350+
final StreamController<Object?> controller = StreamController<Object?>();
351+
addTearDown(controller.close);
352+
353+
setMockMethodCallHandler(MethodChannel(channel.name, channel.codec), (MethodCall call) async {
354+
switch (call.method) {
355+
case 'listen':
356+
return handler.onListen(call.arguments, MockStreamHandlerEventSink(controller.sink));
357+
case 'cancel':
358+
return handler.onCancel(call.arguments);
359+
default:
360+
throw UnimplementedError('Method ${call.method} not implemented');
361+
}
362+
});
363+
364+
final StreamSubscription<Object?> sub = controller.stream.listen(
365+
(Object? e) => channel.binaryMessenger.handlePlatformMessage(
366+
channel.name,
367+
channel.codec.encodeSuccessEnvelope(e),
368+
null,
369+
),
370+
);
371+
addTearDown(sub.cancel);
372+
sub.onError((Object? e) {
373+
if (e is! PlatformException) {
374+
throw ArgumentError('Stream error must be a PlatformException');
375+
}
376+
channel.binaryMessenger.handlePlatformMessage(
377+
channel.name,
378+
channel.codec.encodeErrorEnvelope(
379+
code: e.code,
380+
message: e.message,
381+
details: e.details,
382+
),
383+
null,
384+
);
385+
});
386+
sub.onDone(() => channel.binaryMessenger.handlePlatformMessage(channel.name, null, null));
387+
}
388+
305389
/// Returns true if the `handler` argument matches the `handler`
306390
/// previously passed to [setMockMessageHandler],
307391
/// [setMockDecodedMessageHandler], or [setMockMethodCallHandler].

packages/flutter_test/test/test_default_binary_messenger_test.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,32 @@ void main() {
6969
expect(result?.buffer.asUint8List(), Uint8List.fromList(<int>[2, 3, 4]));
7070
});
7171

72+
test('Mock StreamHandler is set correctly', () async {
73+
const EventChannel channel = EventChannel('');
74+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockStreamHandler(
75+
channel,
76+
MockStreamHandler.inline(onListen: (Object? arguments, MockStreamHandlerEventSink events) {
77+
events.success(arguments);
78+
events.error(code: 'code', message: 'message', details: 'details');
79+
events.endOfStream();
80+
})
81+
);
82+
83+
expect(
84+
channel.receiveBroadcastStream('argument'),
85+
emitsInOrder(<Object?>[
86+
'argument',
87+
emitsError(
88+
isA<PlatformException>()
89+
.having((PlatformException e) => e.code, 'code', 'code')
90+
.having((PlatformException e) => e.message, 'message', 'message')
91+
.having((PlatformException e) => e.details, 'details', 'details'),
92+
),
93+
emitsDone,
94+
]),
95+
);
96+
});
97+
7298
testWidgets('Mock AllMessagesHandler is set correctly',
7399
(WidgetTester tester) async {
74100
final TestDefaultBinaryMessenger binaryMessenger =

0 commit comments

Comments
 (0)