Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Really hate static methods... #722

Closed
jaumard opened this issue Apr 28, 2021 · 4 comments
Closed

Really hate static methods... #722

jaumard opened this issue Apr 28, 2021 · 4 comments

Comments

@jaumard
Copy link

jaumard commented Apr 28, 2021

Previous version of the plugin I could instantiate a geolocator, was nice and my unit tests were really happy about it because geolocator could be mocked.

Now everything is static... Is there any new way that doesn't involve static methods ?

@mvanbeusekom
Copy link
Member

@jaumard I admit this might not have been the best choice I have made. However in my defence common practise on testing and application architecture in general suggests that you should not mock what you don't own (more reading here).

My suggestion would be to write a wrapper around the geolocator and mock the wrapper instead.

@jaumard
Copy link
Author

jaumard commented Apr 29, 2021

Your link is more about TDD than unit testing in general. Also if we don't mock it the test don't pass I suppose because there is no implementation on it when running test.

Also I want to check that geolocation is called in my code and check what I do with the result is correct. So many reason you want to mock third party deps :)

Also don't mock deps using network and run your tests with no network, it will be fun :D or even with really show network and get 1 hour to execute your tests lol

But yeah I'll do my own wrapper around geolocator, just wondering if something was built in before doing it.

@mvanbeusekom
Copy link
Member

mvanbeusekom commented Apr 29, 2021

The link also suggests to create wrappers/ adapters/ shims for thirds party dependencies and mock those instead, for the specific reasons you mentioned. This way your are mocking only code that you own (as in you are the owner of the wrappers/ adapters/ shims).

This has the following benefits:

  • Abstracts the 3rd party API which makes it easier to replace when necessary;
  • Reduces the surface of the API to only the parts that your application needs, making the API easier to use and understand from the rest of your application;
  • Won't break all your tests when the 3rd party API changes.

I will close this issue, but feel free to continue the discussion ;)

@darwin-morocho
Copy link

darwin-morocho commented Jun 29, 2021

I think that this class could be useful to wrap the static methods and next mock it.

import 'dart:async';

import 'package:geolocator/geolocator.dart';

class GeolocatorWrapper {
  StreamController<Position>? _positionController;
  StreamController<bool>? _serviceEnabledController;
  StreamSubscription? _positionSubscription, _serviceEnabledSubscription;

  /// check if the Location Service is Enabled
  Future<bool> get isLocationServiceEnabled => Geolocator.isLocationServiceEnabled();

  /// Returns a [Future] indicating if the user allows the App to access the device's location.
  Future<LocationPermission> checkPermission() => Geolocator.checkPermission();

  Future<bool> get hasPermission async {
    final status = await checkPermission();
    return status == LocationPermission.always || status == LocationPermission.whileInUse;
  }

  /// Calculates the initial bearing between two points
  double bearing(
    double startLatitude,
    double startLongitude,
    double endLatitude,
    double endLongitude,
  ) =>
      Geolocator.bearingBetween(
        startLatitude,
        startLongitude,
        endLatitude,
        endLongitude,
      );

  /// return an stream to listen the changes in the GPS status
  Stream<bool> get onServiceEnabled {
    _serviceEnabledController ??= StreamController.broadcast();

    // listen the changes in GPS status
    _serviceEnabledSubscription = Geolocator.getServiceStatusStream().listen(
      (event) {
        final enabled = event == ServiceStatus.enabled;
        if (enabled) {
          _notifyServiceEnabled(true);
          if (_positionController != null) {
            _initLocationUpdates();
          }
        }
      },
    );

    return _serviceEnabledController!.stream;
  }

  /// return an stream to listen the changes in the current position
  Stream<Position> get onLocationUpdates {
    _positionController ??= StreamController.broadcast();
    _initLocationUpdates();
    return _positionController!.stream;
  }

  /// start listening the position changes
  void _initLocationUpdates() async {
    await _positionSubscription?.cancel();
    _positionSubscription = Geolocator.getPositionStream().listen(
      (event) {
        _positionController?.sink.add(event);
      },
      onError: (e) {
        if (e is LocationServiceDisabledException) {
          _notifyServiceEnabled(false);
        }
      },
    );
  }

  /// notify to all listeners that the location service has changed
  void _notifyServiceEnabled(bool enabled) {
    if (_serviceEnabledController != null) {
      _serviceEnabledController!.sink.add(enabled);
    }
  }

  Future<bool> openAppSettings() => Geolocator.openAppSettings();
  Future<LocationPermission> requestPermission() => Geolocator.requestPermission();
  Future<bool> openLocationSettings() => Geolocator.openLocationSettings();

  /// returns the current position
  Future<Position?> getCurrentPosition({
    LocationAccuracy desiredAccuracy = LocationAccuracy.best,
    bool forceAndroidLocationManager = false,
    Duration? timeLimit,
  }) async {
    try {
      // getCurrentPosition throws an exception when the location service are disabled
      final position = await Geolocator.getCurrentPosition(
        desiredAccuracy: desiredAccuracy,
        forceAndroidLocationManager: forceAndroidLocationManager,
        timeLimit: timeLimit,
      );
      return position;
    } catch (e) {
      return null;
    }
  }

  /// Returns the last known position stored on the users device.
  Future<Position?> getLastKnowPosition({bool forceAndroidLocationManager = false}) async {
    return Geolocator.getLastKnownPosition(
      forceAndroidLocationManager: forceAndroidLocationManager,
    );
  }

  /// release the controllers and cancel all subscribers
  void dispose() {
    _positionController?.close();
    _serviceEnabledSubscription?.cancel();
    _serviceEnabledController?.close();
    _positionSubscription?.cancel();
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants