Dart test fails even though it throws expected exception - dart

I have created the following dart test...
import 'package:is_a_test/is_a_test.dart';
import 'package:test/test.dart';
class MyException implements Exception {
}
void throws() {
throw MyException();
}
void main() {
test('calculate', () {
expect(() => throws(), throwsA(isA<MyException>));
});
}
The expected result would be a test pass. But instead it fails with...
Expected: throws <Closure: () => TypeMatcher<MyException> from Function 'isA': static.>
Actual: <Closure: () => void>
Which: threw <Instance of 'MyException'>
stack test/is_a_test_test.dart 9:3 throws
test/is_a_test_test.dart:9
main.<fn>.<fn>

isA<MyException> is a function that returns a Matcher. The failure indicated this, although it might be hard to understand:
Expected: throws <Closure: () => TypeMatcher<MyException> from Function 'isA': static.>
This means that you were expecting something that throws a closure, essentially a function.
You instead need to invoke that function by using isA<MyException>().

Related

Difference between throwing and returning a Future.error

Minimal reproducible code
Future<void> foo() async {
final fooError = Future.error('FooError');
return Future.error(fooError);
}
Future<void> bar() async {
await Future(() {});
throw Future.error('BarError');
}
void main() {
foo().catchError((e) => (e as Future).catchError(print)); // Error is NOT handled.
bar().catchError((e) => (e as Future).catchError(print)); // Error is handled.
}
As you can see, BarError is handled but not the FooError. AFAIK, when you mark a method async and use throw, the error is wrapped in a Future.error(...). But my first code doesn't work.
Future.error() creates a Future that completes with an error during a future microtask. This means that if the created Future doesn't have a listener when the microtask is executed, it's considered unhandled and will cause the program to terminate. I'm not 100% sure what's triggering the microtask to be executed, but the microtask queue is always executed and drained until it's empty before executing the next event in the isolate's event loop.
However, those details aren't terribly important. To fix this issue, update your example to not wrap your Future.error('FooError') with another Future.error(...), things work as expected:
Future<void> foo() async {
return Future.error('FooError');
}
Future<void> bar() async {
await Future(() {});
throw 'BarError';
}
void main() {
foo().catchError(print); // Error is now handled.
bar().catchError(print); // Error is handled.
}

Testing exceptions in Dart

When writing tests around code that will throw an exception, how can Dart/Mockito(or anything else) avoid throwing a real exception?
For example, these tests should both pass and detect the thrown exception - but Dart throws a real exception in the 1st test so only 'It receives a Todo' passes.
void main() {
test('It throws an exception', () async {
final client = MockClient();
when(client.get(Uri.parse('https://jsonplaceholder.typicode.com/todos/1'))).thenAnswer((_) async => http.Response('', 404));
expect(await fetchTodo(client, 1), throwsException);
});
test('It receives a Todo', () async {
final client = MockClient();
final jsonString = '''
{
"id": 1,
"userId": 1,
"title": "test",
"completed": false
}
''';
when(client.get(Uri.parse('https://jsonplaceholder.typicode.com/todos/2'))).thenAnswer((_) async => http.Response(jsonString, 200));
expect(await fetchTodo(client, 2), isA<Todo>());
});
}
and the mocked get method(based on mockito's generated code - I get the same results when using #GenerateMocks([http.Client]) in my test file.
class MockClient extends Mock implements http.Client {
Future<http.Response> get(Uri url, {Map<String, String>? headers}) {
return super.noSuchMethod(Invocation.method(#get, [url], {#headers: headers}), returnValue: Future.value(http.Response('', 200))) as Future<http.Response>;
}
}
class Todo {
int id;
int userId;
String title;
bool completed;
Todo(this.id, this.userId, this.title, this.completed);
}
Future<Todo> fetchTodo(http.Client client, int id) async {
final response = await client.get(Uri.parse('https://jsonplaceholder.typicode.com/todos/$id'));
if(response.statusCode == 200) {
return Todo(1, 1, 'Test', true);
}else {
throw Exception('Failed to fetch resource');
}
}
Test run report:
00:00 +0: It throws an exception
00:00 +0 -1: It throws an exception [E]
Exception: Failed to fetch resource
test/test.dart 49:5 fetchTodo
00:00 +0 -1: It receives a Todo
00:00 +1 -1: Some tests failed.
Your problem is that you do:
expect(await fetchTodo(client, 1), throwsException);
expect() is a normal function, and function arguments are evaluated before the function is invoked. (Dart is an applicative-order language.) You therefore must wait for fetchTodo to complete before calling expect() (and before expect can try to match against the throwsException Matcher).
As explained by the throwsA documentation (and this applies to the throwsException Matcher too), it must be matched against a zero-argument function or a Future. You do not need to (and should not) await the call to fetchTodo.
Also, since you're calling an asynchronous function, the expectation cannot be checked synchronously, so you'll also need to use expectLater instead of expect:
await expectLater(fetchTodo(client, 1), throwsException);

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.
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"));
});
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>()));
});
}

Why can Iterable<DeclarationMirror> not be cast to Iterable<MethodMirror>?

This code does not work on the Dart VM (1.22.0-dev.9.0), but does work on DartPad (unknown version):
import 'dart:mirrors';
class Thing {
Thing();
}
void g(ClassMirror c) {
var constructors = c.declarations.values
.where((d) => d is MethodMirror && d.isConstructor) as Iterable<MethodMirror>;
print(constructors);
}
void main() {
g(reflectClass(Thing));
}
Results in:
Unhandled exception:
type 'WhereIterable<DeclarationMirror>' is not a subtype of type 'Iterable<MethodMirror>' in type cast where
WhereIterable is from dart:_internal
DeclarationMirror is from dart:mirrors
Iterable is from dart:core
MethodMirror is from dart:mirrors
#0 Object._as (dart:core-patch/object_patch.dart:76)
#1 g (file:///google/src/cloud/srawlins/strong/google3/b.dart:9:55)
#2 main (file:///google/src/cloud/srawlins/strong/google3/b.dart:14:3)
#3 _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:261)
#4 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:148)
(but in DartPad results in (MethodMirror on 'Thing').)
Note that if I hand-craft some classes that implement each other, and do the same thing, it works:
abstract class DM {
bool get t;
}
abstract class MM implements DM {
MM();
bool get t;
}
class _MM implements MM {
bool get t => true;
}
void f(Map<dynamic, DM> dms) {
var mms = dms.values.where((dm) => dm is MM && dm.t) as Iterable<MM>;
print(mms);
}
void main() {
f({1: new _MM()});
}
which nicely prints: (Instance of '_MM')
Just because the iterable returned by .where() can only contain MethodMirror instances, doesn't allow the cast. The type is propagated from c.declarations.values which is DeclarationMirror.
While you can cast a DeclarationMirror to a MethodMirror, a cast from Iterable<DeclarationMirror> to Iterable<MethodMirror> is invalid, because there is no is-a relationship between these to iterables.
It seems when built to JS by dart2js, some generic types are dropped, this is why this works in DartPad.
You can create a new List<MethodMirror> like
import 'dart:mirrors';
class Thing {
Thing();
}
void g(ClassMirror c) {
var constructors = new List<MethodMirror>.from(
c.declarations.values.where((d) => d is MethodMirror && d.isConstructor));
print(constructors);
}
void main() {
g(reflectClass(Thing));
}
There is an open issue to make this easier https://github.com/dart-lang/sdk/issues/27489

Async/future error handling in Dart not working as expected

I've been at this for hours studying the Futures and Error Handling section on the Dart page without any luck. Can anyone explain why the code below does not print All good?
import 'dart:async';
main() async {
try {
await func1();
} catch (e) {
print('All good');
}
}
Future func1() {
var completer = new Completer();
func2().catchError(() => completer.completeError('Noo'));
return completer.future;
}
Future func2() {
var completer = new Completer();
completer.completeError('Noo');
return completer.future;
}
In func1 the function used as catchError parameter must be a subtype of type (dynamic) => dynamic regarding the error:
Unhandled exception:
type '() => dynamic' is not a subtype of type '(dynamic) => dynamic' of 'f'.
Thus you should use:
Future func1() {
var completer = new Completer();
func2().catchError((e) => completer.completeError('Noo'));
return completer.future;
}
You don't get any analyzer error because the parameter is typed with Function. You can file an issue to know why the type is not a more specific type matching (dynamic)=>dynamic

Resources