Search code examples
unit-testingdartmockito-dart

How do I stub callbacks of a method?


I am using Firebase Phone Auth in my Flutter project and want to test my auth class. I know how to use when() and .thenAnswer() from Mockito with typical Futures.

I want to test my authentication method, in particular, verificationFailed and verificationCompleted callbacks.

Future<void> getSmsCodeWithFirebase() async {
    try {
      await _firebaseAuth.verifyPhoneNumber(
        phoneNumber: fullPhoneNumber,
        timeout: const Duration(seconds: 30), 
        verificationCompleted: (credential) async {
          _firebaseSignIn(credential);
        },
        verificationFailed: (e) {
              errorMessage = 'Error code: ${e.code}';
          }
          initModelState = DataState.error;
        },
        codeSent: (String verificationId, int resendToken) {
          _firebaseSessionId = verificationId;
          initModelState = DataState.idle;
        },
        codeAutoRetrievalTimeout: (String verificationId) {},
      );
    } catch (ex) {
      errorMessage = 'There was some error';
      updateModelState = DataState.error;
    }
  }

For now I came up with something like this, but I don't understand how to invoke passed callbacks.

    test('cant verify phonenumber', () async {
      when(mockFirebaseAuth.verifyPhoneNumber(
              phoneNumber: any,
              codeSent: anyNamed('codeSent'),
              verificationCompleted: anyNamed('verificationCompleted'),
              verificationFailed: anyNamed('verificationFailed'),
              codeAutoRetrievalTimeout: anyNamed('codeAutoRetrievalTimeout')))
          .thenAnswer((Invocation invocation) {
        // I need to put something here?
      });
      await authCodeViewModel.getSmsCodeWithFirebase();
      expect(authCodeViewModel.initModelState, DataState.error);
    });

Solution

  • You're not really asking how to stub callbacks themselves; you're asking how to invoke callbacks for a stubbed method. You'd use captured callback arguments the same as any other captured arguments:

    // Sample class with a method that takes a callback.
    abstract class Foo {
      void f(String Function(int x) callback, int y);
    }
    
    @GenerateMocks([Foo])
    void main() {
      var mockFoo = MockFoo();
      mockFoo.f((x) => '$x', 42);
      var captured = verify(mockFoo.f(captureAny, any)).captured;
      var f = captured[0] as String Function(int);
      print(f(88)); // Prints: 88
    }
    

    In your case, I think it'd be something like:

        test('cant verify phonenumber', () async {
          await authCodeViewModel.getSmsCodeWithFirebase();
          var captured = verify(mockFirebaseAuth.verifyPhoneNumber(
                  phoneNumber: any,
                  codeSent: anyNamed('codeSent'),
                  verificationCompleted: anyNamed('verificationCompleted'),
                  verificationFailed: captureNamed('verificationFailed'),
                  codeAutoRetrievalTimeout: anyNamed('codeAutoRetrievalTimeout')))
              .captured;
          var verificationFailed = captured[0] as PhoneVerificationFailed;
          verificationFailed(FirebaseAuthException());
          expect(authCodeViewModel.initModelState, DataState.error);
        });
    

    Of course, if you're supplying the callbacks, you don't need to capture them in the first place; you can just invoke them directly yourself.