dart-lang / mockito

Mockito-inspired mock library for Dart
https://pub.dev/packages/mockito
Apache License 2.0
623 stars 160 forks source link

How to test retrofit services with mockito #715

Closed irangareddy closed 7 months ago

irangareddy commented 7 months ago

Regarding how to write valid tests with Mockito to test the retrofit service, I have perused an ample amount of online resources and read a few blogs, but to no avail. I attempted to write tests for both the service and repository, but encountered the same error each time. I have exhausted all available options, including closed and open, in an attempt to rectify the issue. Please provide assistance in writing this test effectively.

Version

environment:
  sdk: ">=3.1.0 <4.0.0"

dependencies:
  bloc: ^8.1.2
  dio: ^5.3.3
  flutter:
    sdk: flutter
  flutter_bloc: ^8.1.3
  flutter_localizations:
    sdk: flutter
  freezed_annotation: ^2.4.1
  get_it: ^7.6.4
  go_router: ^12.1.1
  intl: ^0.18.0
  json_annotation: ^4.8.1
  logger: ^2.0.2+1
  mockito: ^5.4.2
  provider: ^6.0.5
  retrofit: ^4.0.3
  shared_preferences: ^2.2.2
  storybook_flutter: ^0.14.0

dev_dependencies:
  bloc_test: ^9.1.4
  build_runner: ^2.4.6
  flutter_test:
    sdk: flutter
  freezed: ^2.4.5
  json_serializable: ^6.7.1
  mocktail: ^1.0.0
  retrofit_generator: ^8.0.3
  very_good_analysis: ^5.1.0

Added all required code samples for understanding the code properly

Issue while running tests

type 'Null' is not a subtype of type 'Future<HttpResponse<OnSuccess>>'
test/auth_service_test.dart 9:7    MockAuthService.registerPhoneNumber
test/auth_service_test.dart 29:16  main.<fn>.<fn>

AuthServiceTest

import 'package:dio/dio.dart';
import '../data/data_sources.dart';
import '../utils/utils.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:retrofit/retrofit.dart';

class MockAuthService extends Mock implements AuthService {}

class MockAuthRepository extends Mock implements AuthRepository {}

@GenerateMocks([MockAuthService, MockAuthRepository])
void main() {
  late MockAuthService mockAuthService;
  late MockAuthRepository mockAuthRepository;

  setUp(() {
    mockAuthService = MockAuthService();
    mockAuthRepository = MockAuthRepository();
  });

  group('Auth Validation Tests', () {
    const validMobileNumber = 'xxxxxxxxxxxxx';

    test('sucessfully OTP Sent', () async {
      // Arrange
      when(
        mockAuthService.registerPhoneNumber(Mobile(mobile: validMobileNumber)),
      ).thenReturn(
        Future.value(
          HttpResponse(
            OnSuccess(
              isSuccess: true,
              message: 'Successfully OTP sent to the user.',
            ),
            Response(requestOptions: RequestOptions()),
          ),
        ),
      );

      // Act
      final result = await mockAuthRepository.phoneLogin(validMobileNumber);

      // Assert
      expect((result as DataSuccess).data, isA<OnSuccess>());
      verify(
        mockAuthService.registerPhoneNumber(Mobile(mobile: validMobileNumber)),
      );
    });
  });
}

Models

OnSuccess

import 'package:json_annotation/json_annotation.dart';

part 'on_success.g.dart';

@JsonSerializable()
class OnSuccess {
  OnSuccess({
    required this.isSuccess,
    required this.message,
  });
  factory OnSuccess.fromJson(Map<String, dynamic> json) =>
      _$OnSuccessFromJson(json);
  @JsonKey(name: 'isSuccess')
  bool isSuccess;
  @JsonKey(name: 'message')
  String message;

  Map<String, dynamic> toJson() => _$OnSuccessToJson(this);
}

Mobile

import 'package:json_annotation/json_annotation.dart';

part 'mobile.g.dart';

@JsonSerializable()
class Mobile {
  Mobile({
    required this.mobile,
  });

  factory Mobile.fromJson(Map<String, dynamic> json) => _$MobileFromJson(json);

  @JsonKey(name: 'mobile')
  String mobile;

  Map<String, dynamic> toJson() => _$MobileToJson(this);
}

Generics

DataState

import 'package:dio/dio.dart';

abstract class DataState<T> {
  const DataState({this.data, this.exception});

  final T? data;
  final DioException? exception;
}

class DataSuccess<T> extends DataState<T> {
  const DataSuccess(T data) : super(data: data);
}

class DataFailed<T> extends DataState<T> {
  const DataFailed(DioException exception) : super(exception: exception);
}

class DataLoading<T> extends DataState<T> {
  const DataLoading() : super();
}

Backend

AuthService

import 'package:dio/dio.dart';
import 'package:feebac/data/auth/models/mobile.dart';
import 'package:feebac/data/states/on_success.dart';
import 'package:retrofit/retrofit.dart';

part 'auth_service.g.dart';

@RestApi(baseUrl: K.BASE_URL)
abstract class AuthService {
  factory AuthService(Dio dio, {String baseUrl}) = _AuthService;

  @POST('/login')
  Future<HttpResponse<OnSuccess>> registerPhoneNumber(
    @Body() Mobile mobile,
  );

  @POST('/verify-otp')
  Future<HttpResponse<OnSuccess>> verifyOTP(
    @Body() Map<String, dynamic> requestBody,
  );

}

AuthRepository

import 'dart:io';
import 'package:dio/dio.dart';
import '../data/data_sources.dart';
import '../utils/utils.dart';

class AuthRepository {
  AuthRepository(this._authService);
  final AuthService _authService;

  Future<DataState<OnSuccess>> phoneLogin(String mobileNumber) async {
    try {
      final httpResponse = await _authService.registerPhoneNumber(
        Mobile(mobile: mobileNumber),
      );

      if (httpResponse.response.statusCode == HttpStatus.created) {
        return DataSuccess(httpResponse.data);
      } else {
        return DataFailed(
          DioException(
            error: httpResponse.response.statusMessage,
            response: httpResponse.response,
            requestOptions: httpResponse.response.requestOptions,
          ),
        );
      }
    } on DioException catch (e) {
      return DataFailed(e);
    }
  }
}
srawlins commented 7 months ago

With the error:

type 'Null' is not a subtype of type 'Future<HttpResponse<OnSuccess>>'

and the signature of the method being stubbed:

Future<HttpResponse<OnSuccess>> registerPhoneNumber(Mobile mobile);

My guess is that the generated Mockito code is not creating a proper stub for registerPhoneNumber.

Can you clarify one thing? Where does HttpResponse come from? My guess is dart:io, but that is not shown as an import in the "AuthService" file. Is there a missing import? A missing import could result in the error you show.

If the missing import was just in pasting into GitHub, can you show the contents of the generated mock? It's probably a huge file, so I think just the imports and the registerPhoneNumber override body should be sufficient.

irangareddy commented 7 months ago

HttpResponse comes from import 'package:dio/dio.dart';

GeneratedMock

// Mocks generated by Mockito 5.4.2 from annotations
// in feebac/test/auth_service_test.dart.
// Do not manually edit this file.

// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i5;

import 'package:feebac/data/data_sources.dart' as _i6;
import 'package:feebac/utils/utils.dart' as _i3;
import 'package:mockito/mockito.dart' as _i1;
import 'package:retrofit/retrofit.dart' as _i2;

import 'auth_service_test.dart' as _i4;

// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
// ignore_for_file: comment_references
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class

class _FakeHttpResponse_0<T> extends _i1.SmartFake
    implements _i2.HttpResponse<T> {
  _FakeHttpResponse_0(
    Object parent,
    Invocation parentInvocation,
  ) : super(
          parent,
          parentInvocation,
        );
}

class _FakeDataState_1<T> extends _i1.SmartFake implements _i3.DataState<T> {
  _FakeDataState_1(
    Object parent,
    Invocation parentInvocation,
  ) : super(
          parent,
          parentInvocation,
        );
}

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

  @override
  _i5.Future<_i2.HttpResponse<_i6.OnSuccess>> registerPhoneNumber(
          _i6.Mobile? mobile) =>
      (super.noSuchMethod(
        Invocation.method(
          #registerPhoneNumber,
          [mobile],
        ),
        returnValue: _i5.Future<_i2.HttpResponse<_i6.OnSuccess>>.value(
            _FakeHttpResponse_0<_i6.OnSuccess>(
          this,
          Invocation.method(
            #registerPhoneNumber,
            [mobile],
          ),
        )),
      ) as _i5.Future<_i2.HttpResponse<_i6.OnSuccess>>);

  @override
  _i5.Future<_i2.HttpResponse<_i6.OnSuccess>> verifyOTP(
          Map<String, dynamic>? requestBody) =>
      (super.noSuchMethod(
        Invocation.method(
          #verifyOTP,
          [requestBody],
        ),
        returnValue: _i5.Future<_i2.HttpResponse<_i6.OnSuccess>>.value(
            _FakeHttpResponse_0<_i6.OnSuccess>(
          this,
          Invocation.method(
            #verifyOTP,
            [requestBody],
          ),
        )),
      ) as _i5.Future<_i2.HttpResponse<_i6.OnSuccess>>);
}

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

  @override
  _i5.Future<_i3.DataState<_i6.OnSuccess>> phoneLogin(String? mobileNumber) =>
      (super.noSuchMethod(
        Invocation.method(
          #phoneLogin,
          [mobileNumber],
        ),
        returnValue: _i5.Future<_i3.DataState<_i6.OnSuccess>>.value(
            _FakeDataState_1<_i6.OnSuccess>(
          this,
          Invocation.method(
            #phoneLogin,
            [mobileNumber],
          ),
        )),
      ) as _i5.Future<_i3.DataState<_i6.OnSuccess>>);
}
srawlins commented 7 months ago

Oh whoa I totally missed this:

class MockAuthService extends Mock implements AuthService {}

class MockAuthRepository extends Mock implements AuthRepository {}

@GenerateMocks([MockAuthService, MockAuthRepository])

The mocks should either be manually created or generated, not both. I would remove the two class delcarations here, and generate the mocks with

@GenerateNiceMocks([AuthService, AuthRepository])

We should maybe add a check that classes which already inherit from Mock cannot be used to generate mocks.

irangareddy commented 7 months ago

Thanks @srawlins, Kind of my code has started working, but I have an exception even on a valid phone number too. Am I missing here exactly

Exception
Exception has occurred.
MissingStubError (MissingStubError: 'phoneLogin'
No stub was found which matches the arguments of this method call:
phoneLogin('xxxxxxxx')

Add a stub for this method using Mockito's 'when' API, or generate the MockAuthRepository mock with the @GenerateNiceMocks annotation (see https://pub.dev/documentation/mockito/latest/annotations/MockSpec-class.html).)
@GenerateMocks([AuthService, AuthRepository])
void main() {
  late MockAuthService mockAuthService;
  late MockAuthRepository mockAuthRepository;

  setUpAll(() {
    mockAuthService = MockAuthService();
    mockAuthRepository = MockAuthRepository();
  });

  group('Auth Validation Tests', () {
    const validMobileNumber = 'xxxxxxxx';
    final response = HttpResponse(
      OnSuccess(
        isSuccess: true,
        message: 'Successfully OTP sent to the user.',
      ),
      Response(requestOptions: RequestOptions()),
    );

    test('sucessfully OTP Sent', () async {
      // Arrange
      when(
        mockAuthService.registerPhoneNumber(Mobile(mobile: validMobileNumber)),
      ).thenAnswer((_) async => response);

      // Act
      final result = await mockAuthRepository.phoneLogin(validMobileNumber);

      // Assert
      expect(result.data, isA<OnSuccess>());
      verify(
        mockAuthService.registerPhoneNumber(Mobile(mobile: validMobileNumber)),
      );
    });
  });
}
srawlins commented 7 months ago

I'm glad we got past your code generation issue. At this point, I can't offer any further support. User forums like StackOverflow are a better place to ask questions like this.

Exception has occurred. MissingStubError (MissingStubError: 'phoneLogin' No stub was found which matches the arguments of this method call: phoneLogin('xxxxxxxx')

Add a stub for this method using Mockito's 'when' API, or generate the MockAuthRepository mock with the @GenerateNiceMocks annotation (see https://pub.dev/documentation/mockito/latest/annotations/MockSpec-class.html).)

final result = await mockAuthRepository.phoneLogin(validMobileNumber);

I can only repeat the error message to explain what went wrong: No stub was found for phoneLogin. The method was called, but no stub was set up.

irangareddy commented 7 months ago

I can't thank you enough for this great help. If possible, I will write a good article around this. Thank you so much.