flutterdarttestingmqtt

Mocking platform specific packages in flutter test


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 = {};
}

Solution

  • 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.