I am trying to test the behavior of a singleton in dart:
import 'dart:async';
import 'package:mqtt_client/mqtt_browser_client.dart';
class MqttService {
static final MqttService _instance = MqttService._internal();
factory MqttService() => _instance;
MqttService._internal();
final Map<String, MqttBrowserClient> clients = {};
final Map<String, StreamController<int>> controllers = {};
final Map<String, int> lastKnownAmounts = {};
}
I've written the following test:
import 'package:flutter_test/flutter_test.dart';
import 'package:common/mqtt_service.dart';
void main() {
test('Should not create additional instances of MqttService', () {
final service1 = MqttService();
final service2 = MqttService();
expect(service1.hashCode, service2.hashCode);
});
}
However, when running it, I get a giant error (I won't paste everything here, it should be reproducible by the example)
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/xhr.dart:502:27: Error: 'JSObject' isn't a type.
extension type FormData._(JSObject _) implements JSObject {
^^^^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/xhr.dart:518:5: Error: 'JSAny' isn't a type.
JSAny blobValueOrValue, [
^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/xhr.dart:551:5: Error: 'JSAny' isn't a type.
JSAny blobValueOrValue, [
^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/xhr.dart:565:32: Error: 'JSObject' isn't a type.
extension type ProgressEvent._(JSObject _) implements Event, JSObject {
^^^^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/xhr.dart:605:36: Error: 'JSObject' isn't a type.
extension type ProgressEventInit._(JSObject _) implements EventInit, JSObject {
^^^^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/lists.dart:11:44: Error: 'JSObject' isn't a type.
extension type _JSList<T extends JSObject>(JSObject _) implements JSObject {
^^^^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/cross_origin.dart:10:35: Error: 'JSAny' isn't a type.
extension type _CrossOriginWindow(JSAny? any) {
^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/cross_origin.dart:27:5: Error: 'JSAny' isn't a type.
JSAny? message, [
^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/cross_origin.dart:28:5: Error: 'JSAny' isn't a type.
JSAny optionsOrTargetOrigin,
^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/cross_origin.dart:29:13: Error: 'JSObject' isn't a type.
JSArray<JSObject> transfer,
^^^^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/cross_origin.dart:29:5: Error: 'JSArray' isn't a type.
JSArray<JSObject> transfer,
^^^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/cross_origin.dart:34:37: Error: 'JSAny' isn't a type.
extension type _CrossOriginLocation(JSAny? any) {
^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/cross_origin.dart:51:23: Error: 'JSAny' isn't a type.
CrossOriginWindow._(JSAny? o) : _window = _CrossOriginWindow(o);
^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/cross_origin.dart:53:37: Error: 'JSAny' isn't a type.
static CrossOriginWindow? _create(JSAny? o) {
^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/cross_origin.dart:96:5: Error: 'JSAny' isn't a type.
JSAny? message, [
^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/cross_origin.dart:97:5: Error: 'JSAny' isn't a type.
JSAny? optionsOrTargetOrigin,
^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/cross_origin.dart:98:13: Error: 'JSObject' isn't a type.
JSArray<JSObject>? transfer,
^^^^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/cross_origin.dart:98:5: Error: 'JSArray' isn't a type.
JSArray<JSObject>? transfer,
^^^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/cross_origin.dart:132:25: Error: 'JSAny' isn't a type.
CrossOriginLocation._(JSAny? o) : _location = _CrossOriginLocation(o);
^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/cross_origin.dart:134:39: Error: 'JSAny' isn't a type.
static CrossOriginLocation? _create(JSAny? o) {
^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/events/providers.dart:615:25: Error: 'JSObject' isn't a type.
final jsObject = e as JSObject;
^^^^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/events/providers.dart:618:47: Error: The getter 'toJS' isn't defined for the class 'String'.
Try correcting the name to the name of an existing getter, or defining a getter or field named 'toJS'.
} else if (jsObject.hasProperty('mozHidden'.toJS).toDart) {
^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/events/providers.dart:620:46: Error: The getter 'toJS' isn't defined for the class 'String'.
Try correcting the name to the name of an existing getter, or defining a getter or field named 'toJS'.
} else if (jsObject.hasProperty('msHidden'.toJS).toDart) {
^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/events/providers.dart:622:50: Error: The getter 'toJS' isn't defined for the class 'String'.
Try correcting the name to the name of an existing getter, or defining a getter or field named 'toJS'.
} else if (jsObject.hasProperty('webkitHidden'.toJS).toDart) {
^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/events/streams.dart:119:7: Error: A value of type '_EventStream<T>' can't be returned from a function with return type 'Stream<T>' because 'T' is nullable and 'T' isn't.
- '_EventStream' is from 'package:web/src/helpers/events/streams.dart' ('../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/events/streams.dart').
- 'Stream' is from 'dart:async'.
this;
^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/events/streams.dart:165:69: Error: The getter 'toJS' isn't defined for the class 'void Function(Event)'.
Try correcting the name to the name of an existing getter, or defining a getter or field named 'toJS'.
: _wrapZone<html.Event>((e) => (onData as dynamic)(e))?.toJS {
^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/events/streams.dart:218:69: Error: The getter 'toJS' isn't defined for the class 'void Function(Event)'.
Try correcting the name to the name of an existing getter, or defining a getter or field named 'toJS'.
: _wrapZone<html.Event>((e) => (handleData as dynamic)(e))?.toJS;
^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/events/streams.dart:253:66: Error: The getter 'toJS' isn't defined for the class 'bool'.
Try correcting the name to the name of an existing getter, or defining a getter or field named 'toJS'.
_target!.addEventListener(_eventType, _onData, _useCapture.toJS);
^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/events/streams.dart:259:69: Error: The getter 'toJS' isn't defined for the class 'bool'.
Try correcting the name to the name of an existing getter, or defining a getter or field named 'toJS'.
_target!.removeEventListener(_eventType, _onData, _useCapture.toJS);
^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/events/streams.dart:441:38: Error: The argument type 'void Function(T)?' can't be assigned to the parameter type 'void Function(T)?' because 'T' is nullable and 'T' isn't.
streamController.stream.listen(onData,
^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/events/streams.dart:441:31: Error: A value of type 'StreamSubscription<T>' can't be returned from a function with return type 'StreamSubscription<T>' because 'T' is nullable and 'T' isn't.
- 'StreamSubscription' is from 'dart:async'.
streamController.stream.listen(onData,
^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/events/streams.dart:448:24: Error: A value of type 'Stream<T>' can't be returned from a function with return type 'Stream<T>' because 'T' is nullable and 'T' isn't.
- 'Stream' is from 'dart:async'.
streamController.stream;
^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/extensions.dart:39:69: Error: The getter 'toJS' isn't defined for the class 'num'.
Try correcting the name to the name of an existing getter, or defining a getter or field named 'toJS'.
(quality == null) ? toDataURL(type) : toDataURL(type, quality.toJS);
^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/extensions.dart:55:7: Error: The method 'jsify' isn't defined for the class 'Map<String, bool>'.
- 'Map' is from 'dart:core'.
Try correcting the name to the name of an existing method, or defining a method named 'jsify'.
}.jsify();
^^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/http.dart:245:33: Error: The argument type 'ProgressEvent' can't be assigned to the parameter type 'Object' because 'ProgressEvent' is nullable and 'Object' isn't.
- 'Object' is from 'dart:core'.
completer.completeError(e);
^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/http.dart:249:34: Error: The argument type 'void Function(Object, [StackTrace?])' can't be assigned to the parameter type 'void Function(ProgressEvent)?' because 'ProgressEvent' is nullable and 'Object' isn't.
- 'Object' is from 'dart:core'.
- 'StackTrace' is from 'dart:core'.
xhr.onError.listen(completer.completeError);
^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/http.dart:252:46: Error: The getter 'toJS' isn't defined for the class 'String'.
Try correcting the name to the name of an existing getter, or defining a getter or field named 'toJS'.
xhr.send(sendData is String ? sendData.toJS : sendData.jsify());
^^^^
../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/helpers/http.dart:252:62: Error: The method 'jsify' isn't defined for the class 'Object'.
- 'Object' is from 'dart:core'.
Try correcting the name to the name of an existing method, or defining a method named 'jsify'.
xhr.send(sendData is String ? sendData.toJS : sendData.jsify());
^^^^^
Digging a little bit, the culprit seems like the MqttBrowserClient class, removing it makes the test run. Apparently, it is because the test is importing a mqtt package specific to the web (my project is web only). How can I force it to import the "correct" library to run tests?
Example of working test:
import 'dart:async';
//import 'package:mqtt_client/mqtt_browser_client.dart';
class MqttService {
static final MqttService _instance = MqttService._internal();
factory MqttService() => _instance;
MqttService._internal();
//final Map<String, MqttBrowserClient> clients = {};
final Map<String, StreamController<int>> controllers = {};
final Map<String, int> lastKnownAmounts = {};
}
The solution ended up being simpler than I thought...
After digging a little bit, MqttBrowserClient is meant to be used only in browsers (which is indeed my use case), however it extends a base a client (i.e MqttClient). So the fix is to use this base class inside the MqttService:
import 'dart:async';
import 'package:mqtt_client/mqtt_client.dart';
class MqttService {
static final MqttService _instance = MqttService._internal();
factory MqttService() => _instance;
MqttService._internal();
final Map<String, MqttClient> clients = {};
final Map<String, StreamController<int>> controllers = {};
final Map<String, int> lastKnownAmounts = {};
}
The test now runs successfully.
Edit
I was also having an issue with a component in which I had to create a MqttBrowserClient, making testing with it not possible. To fix that issue I resorted to the abstract factory design pattern, passing to the component an MqttFactory type. Two concrete classes where created: one for MqttWebFactory and other for testing. The factory only had one method, which would create an MqttClient. This has solved all my issues with testing and the mqtt_client package.