flutterunit-testingflutter-getx

Unit Testing GetxController


I'm a beginner with tdd so please forgive me if it's a dumb question.

I'm having difficulty unit testing GetxControllers. Does anyone know a simple way of doing this? Whenever I do I get errors since Get is calling onStart and it doesn't like the result Mockito's giving it. I've tried using Mockito 5.0.1's auto generated code as well as the older syntax, class MockController extends Mock implements Controller{}, as well as extends Fake.

The auto generated code has build errors, since Mockito is trying to use _InternalFinalCallback, but it's not being imported as it's private. I tried just copy pasting that part of the code into my generated file (and switching off pub build watch) but first that's a short term solution with it's own issues, 2nd it still doesn't work since the onStart and onDelete functions now tell me they're not valid overrides.

Also, I can see the get_test package but it's documentation is basically 0, and in the examples the controller is just used directly -- there's never a mocked controller.

I tried setting Get.testMode = true; but again that doesn't seem to do anything. And while I found that property in the docs, I didn't find how to use it correctly.

Any help would be appreciated,

Here's my code but the issue seems to be with the GetxControllers, so I don't think it's relevant much:

class FakeAuthController extends Fake implements AuthController {}

@GenerateMocks([AuthController])
void main() {
  TestWidgetsFlutterBinding.ensureInitialized();
  late MockAuthController mockAuthController;
  late FakeAuthController fakeAuthController;
  late SessionController sessionController;

  setUp(() {
    Get.testMode = true;
    mockAuthController = MockAuthController();
    fakeAuthController = FakeAuthController();
    Get.put<AuthController>(mockAuthController);

    sessionController = SessionController();
  });

  tearDown(() {
    Get.delete<AuthController>();
  });

  group('getSessionInfo', () {
    test('Calls authFacade getSignedInUserId', () async {
      await sessionController.getSessionInfo();

      when(Get.find<AuthController>()).thenReturn(fakeAuthController);

      verify(mockAuthController.getSignedInUserId());
    });
  });
}

There really isn't anything in my AuthController and session controller, but code is as follows:

import 'package:get/get.dart';

class AuthController extends GetxController {
  String getSignedInUserId() {
    // await Future.delayed(Duration(milliseconds: 1));
    return '1';
  }
}


import 'package:get/get.dart';

import '../../auth/controllers/auth_controller.dart';
import '../models/session_info.dart';

class SessionController extends GetxController {
  final AuthController authController = Get.find<AuthController>();

  Rx<SessionInfo> sessionInfo = Rx<SessionInfo>();

  Future<void> getSessionInfo() async {
    // authController.getSignedInUserId();

    // sessionInfo.value = SessionInfo(userId: userId);
  }
}

And the auto-generated, buggy mock controller:

// Mocks generated by Mockito 5.0.1 from annotations
// in smart_locker_controller/test/shared/controllers/session_controller_test.dart.
// Do not manually edit this file.

import 'dart:ui' as _i4;

import 'package:get/get_instance/src/lifecycle.dart' as _i2;
import 'package:get/get_state_manager/src/simple/list_notifier.dart' as _i5;
import 'package:mockito/mockito.dart' as _i1;
import 'package:smart_locker_controller/auth/controllers/auth_controller.dart'
    as _i3;

// ignore_for_file: comment_references
// ignore_for_file: unnecessary_parenthesis

class _Fake_InternalFinalCallback<T> extends _i1.Fake
    implements _i2._InternalFinalCallback<T> {}

/// A class which mocks [AuthController].
///
/// See the documentation for Mockito's code generation for more information.
class MockAuthController extends _i1.Mock implements _i3.AuthController {
  MockAuthController() {
    _i1.throwOnMissingStub(this);
  }

  @override
  int get notifierVersion =>
      (super.noSuchMethod(Invocation.getter(#notifierVersion), returnValue: 0)
          as int);
  @override
  int get notifierMicrotask =>
      (super.noSuchMethod(Invocation.getter(#notifierMicrotask), returnValue: 0)
          as int);
  @override
  bool get hasListeners =>
      (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false)
          as bool);
  @override
  int get listeners =>
      (super.noSuchMethod(Invocation.getter(#listeners), returnValue: 0)
          as int);
  @override
  _i2._InternalFinalCallback<void> get onStart =>
      (super.noSuchMethod(Invocation.getter(#onStart),
              returnValue: _Fake_InternalFinalCallback<void>())
          as _i2._InternalFinalCallback<void>);
  @override
  _i2._InternalFinalCallback<void> get onDelete =>
      (super.noSuchMethod(Invocation.getter(#onDelete),
              returnValue: _Fake_InternalFinalCallback<void>())
          as _i2._InternalFinalCallback<void>);
  @override
  bool get initialized =>
      (super.noSuchMethod(Invocation.getter(#initialized), returnValue: false)
          as bool);
  @override
  bool get isClosed =>
      (super.noSuchMethod(Invocation.getter(#isClosed), returnValue: false)
          as bool);
  @override
  String getSignedInUserId() =>
      (super.noSuchMethod(Invocation.method(#getSignedInUserId, []),
          returnValue: '') as String);
  @override
  void update([List<Object>? ids, bool? condition = true]) =>
      super.noSuchMethod(Invocation.method(#update, [ids, condition]),
          returnValueForMissingStub: null);
  @override
  void refreshGroup(Object? id) =>
      super.noSuchMethod(Invocation.method(#refreshGroup, [id]),
          returnValueForMissingStub: null);
  @override
  void removeListener(_i4.VoidCallback? listener) =>
      super.noSuchMethod(Invocation.method(#removeListener, [listener]),
          returnValueForMissingStub: null);
  @override
  void removeListenerId(Object? id, _i4.VoidCallback? listener) =>
      super.noSuchMethod(Invocation.method(#removeListenerId, [id, listener]),
          returnValueForMissingStub: null);
  @override
  _i5.Disposer addListener(_i5.GetStateUpdate? listener) =>
      (super.noSuchMethod(Invocation.method(#addListener, [listener]),
          returnValue: () {}) as _i5.Disposer);
  @override
  _i5.Disposer addListenerId(Object? key, _i5.GetStateUpdate? listener) =>
      (super.noSuchMethod(Invocation.method(#addListenerId, [key, listener]),
          returnValue: () {}) as _i5.Disposer);
  @override
  void disposeId(Object? id) =>
      super.noSuchMethod(Invocation.method(#disposeId, [id]),
          returnValueForMissingStub: null);
}

Solution

  • This question has now been answered in the GetX docs.

    Pasted from the docs:

    Tests

    You can test your controllers like any other class, including their lifecycles:

    class Controller extends GetxController {
      @override
      void onInit() {
        super.onInit();
        //Change value to name2
        name.value = 'name2';
      }
    
      @override
      void onClose() {
        name.value = '';
        super.onClose();
      }
    
      final name = 'name1'.obs;
    
      void changeName() => name.value = 'name3';
    }
    
    void main() {
      test('''
    Test the state of the reactive variable "name" across all of its lifecycles''',
          () {
        /// You can test the controller without the lifecycle,
        /// but it's not recommended unless you're not using
        ///  GetX dependency injection
        final controller = Controller();
        expect(controller.name.value, 'name1');
    
        /// If you are using it, you can test everything,
        /// including the state of the application after each lifecycle.
        Get.put(controller); // onInit was called
        expect(controller.name.value, 'name2');
    
        /// Test your functions
        controller.changeName();
        expect(controller.name.value, 'name3');
    
        /// onClose was called
        Get.delete<Controller>();
    
        expect(controller.name.value, '');
      });
    }
    

    Mockito or mocktail

    If you need to mock your GetxController/GetxService, you should extend GetxController, and mixin it with Mock, that way

    class NotificationServiceMock extends GetxService with Mock implements NotificationService {}
    

    Not exactly intuitive, is it?