ssiddh
ssiddh

Reputation: 520

Flutter - Mockito behaves weird when trying to throw custom Exception

Trying to use Mockito to test my BLoC, the BLoC makes a server call using a repository class and the server call function is supposed to throw a custom exception if the user is not authenticated.

But when I am trying to stub the repository function to throw that custom exception, the test just fails with the following error:

sunapsis Authorization error (test error): test description

package:mockito/src/mock.dart 342:7                                     PostExpectation.thenThrow.<fn>
package:mockito/src/mock.dart 119:37                                    Mock.noSuchMethod
package:sunapsis/datasource/models/notifications_repository.dart 28:37  MockNotificationRepository.getNotificationList
package:sunapsis/blocs/notification_blocs/notification_bloc.dart 36:10  NotificationBloc.fetchNotifications
test/blocs/notification_blocs/notification_bloc_test.dart 53:48         main.<fn>.<fn>.<fn>
===== asynchronous gap ===========================
dart:async                                                              scheduleMicrotask
test/blocs/notification_blocs/notification_bloc_test.dart 53:7          main.<fn>.<fn>

And this is what my BLoC code looks like: fetchNotifications function calls the repository function and handles the response and errors. There are two catchError blocks, one handles AuthorizationException case and other handles any other Exception. Handling AuthorizationException differently because it will be used to set the Login state of the application.

notification_bloc.dart

import 'dart:async';

import 'package:logging/logging.dart';
import 'package:rxdart/rxdart.dart';
import 'package:sunapsis/datasource/dataobjects/notification.dart';
import 'package:sunapsis/datasource/models/notifications_repository.dart';
import 'package:sunapsis/utils/authorization_exception.dart';

class NotificationBloc {
  final NotificationsRepository _notificationsRepository;

  final Logger log = Logger('NotificationBloc');
  final _listNotifications = PublishSubject<List<NotificationElement>>();
  final _isEmptyList = PublishSubject<bool>();
  final _isLoggedIn = PublishSubject<bool>();

  Observable<List<NotificationElement>> get getNotificationList =>
      _listNotifications.stream;

  Observable<bool> get isLoggedIn => _isLoggedIn.stream;

  Observable<bool> get isEmptyList => _isEmptyList.stream;

  NotificationBloc({NotificationsRepository notificationsRepository})
      : _notificationsRepository =
            notificationsRepository ?? NotificationsRepository();

  void fetchNotifications() {
    _notificationsRepository
        .getNotificationList()
        .then((List<NotificationElement> list) {
          if (list.length > 0) {
            _listNotifications.add(list);
          } else {
            _isEmptyList.add(true);
          }
        })
        .catchError((e) => _handleErrorCase,
            test: (e) => e is AuthorizationException)
        .catchError((e) {
          log.shout("Error occurred while fetching notifications $e");
          _listNotifications.sink.addError("$e");
        });
  }
  void _handleErrorCase(e) {
     log.shout("Session invalid: $e");
     _isLoggedIn.sink.add(false);
     _listNotifications.sink.addError("Error");
 }
}

This is what my repository code looks like:

notifications_repository.dart

import 'dart:async';

import 'package:logging/logging.dart';
import 'package:sunapsis/datasource/dataobjects/notification.dart';
import 'package:sunapsis/datasource/db/sunapsis_db_provider.dart';
import 'package:sunapsis/datasource/network/api_response.dart';
import 'package:sunapsis/datasource/network/sunapsis_api_provider.dart';
import 'package:sunapsis/utils/authorization_exception.dart';

/// Repository class which makes available all notifications related API functions
/// for server calls and database calls
class NotificationsRepository {
  final Logger log = Logger('NotificationsRepository');
  final SunapsisApiProvider apiProvider;
  final SunapsisDbProvider dbProvider;

  /// Optional [SunapsisApiProvider] and [SunapsisDbProvider] instances expected for unit testing
  /// If instances are not provided - default case - a new instance is created
  NotificationsRepository({SunapsisApiProvider api, SunapsisDbProvider db})
      : apiProvider = api ?? SunapsisApiProvider(),
        dbProvider = db ?? SunapsisDbProvider();

  /// Returns a [Future] of [List] of [NotificationElement]
  /// Tries to first look for notifications on the db
  /// if notifications are found that list is returned
  /// else a server call is made to fetch notifications
  Future<List<NotificationElement>> getNotificationList([int currentTime]) {
    return dbProvider.fetchNotifications().then(
        (List<NotificationElement> notifications) {
      if (notifications.length == 0) {
        return getNotificationsListFromServer(currentTime);
      }
      return notifications;
    }, onError: (_) {
      return getNotificationsListFromServer(currentTime);
    });
  }
}

The function getNotificationsListFromServer is supposed to throw the AuthorizationException, which is supposed to be propagated through getNotificationList

This is the test case that is failing with the error mentioned before:

test('getNotification observable gets error on AuthorizationException',
    () async {
  when(mockNotificationsRepository.getNotificationList())
      .thenThrow(AuthorizationException("test error", "test description"));
  scheduleMicrotask(() => notificationBloc.fetchNotifications());
  await expectLater(
      notificationBloc.getNotificationList, emitsError("Error"));
});

And this is what the custom exception looks like:

authorization_exception.dart

class AuthorizationException implements Exception {
  final String error;

  final String description;

  AuthorizationException(this.error, this.description);

  String toString() {
    var header = 'sunapsis Authorization error ($error)';
    if (description != null) {
      header = '$header: $description';
    }
    return '$header';
  }
}

PS: When I tested my repository class and the function throwing the custom exception those tests were passed.

test('throws AuthorizationException on invalidSession()', () async {
  when(mockSunapsisDbProvider.fetchNotifications())
      .thenAnswer((_) => Future.error("Error"));
  when(mockSunapsisDbProvider.getCachedLoginSession(1536333713))
      .thenAnswer((_) => Future.value(authorization));
  when(mockSunapsisApiProvider.getNotifications(authHeader))
      .thenAnswer((_) => Future.value(ApiResponse.invalidSession()));
  expect(notificationsRepository.getNotificationList(1536333713),
      throwsA(TypeMatcher<AuthorizationException>()));
});

Above test passed and works as expected.

I am a new college grad working my first full time role and I might be doing something wrong. I will really appreciate any feedback or help, everything helps. Thanks for looking into this question.

Upvotes: 3

Views: 3187

Answers (2)

Samuel T.
Samuel T.

Reputation: 362

You're using thenThrow to throw an exception, but because the mocked method returns a Future you should use thenAnswer.

The test would be like that:

test('getNotification observable gets error on AuthorizationException', () async {

  // Using thenAnswer to throw an exception:
  when(mockNotificationsRepository.getNotificationList())
      .thenAnswer((_) async => throw AuthorizationException("test error", "test description"));

  scheduleMicrotask(() => notificationBloc.fetchNotifications());
  await expectLater(notificationBloc.getNotificationList, emitsError("Error"));
});

Upvotes: 3

Simon
Simon

Reputation: 11190

I think you are using the wrong TypeMatcher class. You need to use the one from the testing framework and not the one from the Flutter framework.

import 'package:flutter_test/flutter_test.dart';
import 'package:matcher/matcher.dart';

class AuthorizationException implements Exception {
  const AuthorizationException();
}

Future<List<String>> getNotificationList(int id) async {
  throw AuthorizationException();
}

void main() {
  test('getNotification observable gets error on AuthorizationException',
  () async {
    expect(getNotificationList(1536333713),
      throwsA(const TypeMatcher<AuthorizationException>()));
  });
}

Upvotes: 1

Related Questions