Reputation: 520
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
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
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