I'm working in a team and we are using flutter bloc for state management, however one of our test cases just doesn't make any sense at all. The bloc itself works fine but the test is what's not making any sense.
Below is the bloc itself.
import 'package:vaccify/features/logout/logout.dart';
import 'package:vaccify/features/side_navigation_bar/domain/get_user_name.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:vaccify/features/side_navigation_bar/domain/get_user_profile_pic_url.dart';
part 'auth_event.dart';
part 'auth_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
GetUserName getUserName;
GetUserProfilePic getUserProfilePic;
Logout logout;
AuthBloc(
{required this.getUserName,
required this.logout,
required this.getUserProfilePic})
: super(NotLoggedIn()) {
on<AuthLogIn>(_onLogIn);
on<AuthLogOut>(_onLogOut);
}
void _onLogIn(
AuthLogIn event,
Emitter<AuthState> emit,
) async {
final name = await getUserName();
final profilePic = await getUserProfilePic();
emit(LoggedIn(name, profilePic));
}
void _onLogOut(
AuthLogOut event,
Emitter<AuthState> emit,
) {
logout();
emit(NotLoggedIn());
}
}
And now for the test
import 'package:bloc_test/bloc_test.dart';
import 'package:vaccify/core/bloc/authentication/auth_bloc.dart';
import 'package:vaccify/features/logout/logout.dart';
import 'package:vaccify/features/side_navigation_bar/domain/get_user_name.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:vaccify/features/side_navigation_bar/domain/get_user_profile_pic_url.dart';
class MockGetUserName extends Mock implements GetUserName {}
class MockGetProfilePicUrl extends Mock implements GetUserProfilePic {}
class MockLogout extends Mock implements Logout {}
void main() {
late AuthBloc authBloc;
late MockGetUserName mockGetUserName;
late MockLogout mockLogout;
late MockGetProfilePicUrl mockGetProfilePicUrl;
setUp(() {
mockGetUserName = MockGetUserName();
mockLogout = MockLogout();
mockGetProfilePicUrl = MockGetProfilePicUrl();
authBloc = AuthBloc(
getUserName: mockGetUserName,
logout: mockLogout,
getUserProfilePic: mockGetProfilePicUrl);
});
/// Contains bloc tests for the [AuthBloc] class
group(
'AuthBloc',
() {
/// Tests that a [LoggedIn] state occurs when the [AuthLogIn] is called.
blocTest(
'login',
build: () {
when(() => mockGetUserName()).thenAnswer((_) async => "Hello");
when(() => mockGetProfilePicUrl()).thenAnswer((_) async => "");
return authBloc;
},
act: (AuthBloc bloc) {
bloc.add(AuthLogIn());
},
expect: () => [isA<LoggedIn>()],
);
/// Tests that a [NotLoggedIn] state occurs when the [AuthLogOut] is called.
blocTest(
'logout',
build: () {
when(() => mockGetUserName()).thenAnswer((_) async => "John Smith");
when(() => mockGetProfilePicUrl()).thenAnswer((_) async => "");
when(() => mockLogout()).thenAnswer((_) async {});
return authBloc;
},
act: (AuthBloc bloc) {
bloc.add(AuthLogIn());
bloc.add(AuthLogOut());
},
expect: () => [
isA<LoggedIn>(),
isA<NotLoggedIn>(),
],
);
},
);
}
The first test 'login' works fine and passes because we add one event "AuthLogin()" and then we expect LoggedIn state
The second test 'logout' is the problem because we are adding two events AuthLogin() & AuthLogOut() because you have to be logged in to be able to logout.
Then we expect LoggedIn & NotLoggedIn states respectively.
The test fails with the following message
Expected: [<<Instance of 'LoggedIn'>>, <<Instance of 'NotLoggedIn'>>]
Actual: [Instance of 'NotLoggedIn', Instance of 'LoggedIn']
Interestingly when we swap the two expects the test passes...Like below
expect: () => [
isA<NotLoggedIn>(),
isA<LoggedIn>(),
],
Any advice or guidance will be greatly appreciated.
Thanks all
If you're adding AuthLogIn event just to prepare your bloc for the second event, instead of that you can use seed from blocTest. Something like this:
blocTest(
'logout',
build: () {
when(() => mockLogout()).thenAnswer((_) async {});
return authBloc;
},
seed: () => LoggedIn(),
act: (AuthBloc bloc) {
bloc.add(AuthLogOut());
},
expect: () => [
isA<NotLoggedIn>(),
],
);
And about your problem, I think using a delay between adding two events (await Future.delayed(Duration(seconds: 1))) will fix it. Note that Bloc transforms events concurrently by default and in your case the log out is being processed first (it finishes faster than log in function) and then your log in event is being processed which causes the problem.
Related
I'm trying to run a test with mockito following an outdated tutorial for a messaging app in flutter. I'm trying to use the most updated versions of everything. The tutorial just implemented flutter bloc, and now I'm getting an error that I don't know how to fix.
This is the test:
import 'package:chat/chat.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:rethink_chat/states_management/message/message_bloc.dart';
import 'message_bloc_test.mocks.dart';
#GenerateMocks([IMessageService])
void main() {
late MessageBloc sut;
late MockIMessageService messageService; //changed to Mock
late User user;
setUp(() {
messageService = MockIMessageService(); // FakeMessageService();
user = User(
username: 'test',
photoUrl: '',
active: true,
lastseen: DateTime.now(),
);
sut = MessageBloc(messageService);
});
tearDown(() => sut.close());
test('should emit message sent state when message is sent', () {
final message = Message(
from: '123',
to: '456',
contents: 'test message',
timestamp: DateTime.now(),
);
when(messageService.send(message)).thenAnswer((_) async => true);
when(messageService.dispose()).thenAnswer((_) async => <Object?>[]);
sut.add(MessageEvent.onMessageSent(message));
expectLater(sut.stream, emits(MessageState.sent(message)));
});
}
This is the error:
Bad state: add(MessageSent) was called without a registered event handler.
Make sure to register a handler via on<MessageSent>((event, emit) {...})
This is message_event.dart:
part of 'message_bloc.dart';
abstract class MessageEvent extends Equatable {
const MessageEvent();
factory MessageEvent.onSubscribed(User user) => Subscribed(user);
factory MessageEvent.onMessageSent(Message message) => MessageSent(message);
#override
List<Object> get props => [];
}
class Subscribed extends MessageEvent {
final User user;
const Subscribed(this.user);
#override
List<Object> get props => [user];
}
class MessageSent extends MessageEvent {
final Message message;
const MessageSent(this.message);
#override
List<Object> get props => [message];
}
class _MessageReceived extends MessageEvent {
const _MessageReceived(this.message);
final Message message;
#override
List<Object> get props => [message];
}
And message_bloc.dart:
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:chat/chat.dart';
import 'package:equatable/equatable.dart';
//import 'package:rethink_chat/states_management/message/message_event.dart';
// import 'package:rethink_chat/states_management/message/message_state.dart';
//import 'package:chat/chat.dart';
part 'message_event.dart';
part 'message_state.dart';
class MessageBloc extends Bloc<MessageEvent, MessageState> {
final IMessageService _messageService;
StreamSubscription?
_subscription; // added a ? but not sure if it's neutral, helpful or harmful
MessageBloc(this._messageService) : super(MessageState.initial());
#override
Stream<MessageState> mapEventToState(MessageEvent event) async* {
if (event is Subscribed) {
await _subscription?.cancel();
_subscription = _messageService
.messages(activeUser: event.user)
.listen((message) => add(_MessageReceived(message)));
}
if (event is _MessageReceived) {
yield MessageState.received(event.message);
}
if (event is MessageSent) {
await _messageService.send(event.message);
yield MessageState.sent(event.message);
}
}
#override
Future<void> close() {
_subscription?.cancel();
_messageService.dispose();
return super.close();
}
}
I saw in another post that flutter bloc at some point removed mapEventToState, but I don't know how to fix this for my situation? I'm so new to this that I have trouble knowing what to fix for this particular code, based on seeing examples of other code.
Please let me know if any other information would be helpful and I'll update this post to include it. I'm a total beginner, so this is hopefully a very simple fix that I'm overlooking. I greatly appreciate any help.
Edit: I also get a similar error for a similar test:
test('should emit messages received from service', () {
final message = Message(
from: '123',
to: '456',
contents: 'test message',
timestamp: DateTime.now(),
);
when(messageService.messages(activeUser: anyNamed('activeUser')))
.thenAnswer((_) => Stream.fromIterable([message]));
when(messageService.dispose()).thenAnswer((_) async => <Object?>[]);
sut.add(MessageEvent.onSubscribed(user));
expectLater(sut.stream, emitsInOrder([MessageReceivedSuccess(message)]));
});
The error is
Bad state: add(Subscribed) was called without a registered event handler.
Make sure to register a handler via on<Subscribed>((event, emit) {...})
I assume the fix should be the same for both, right?
Edit2: so I'm thinking I need to put on<MessageSent>((event, emit) {...}) and on<Subscribed>((event, emit) {...}) somewhere in the code, but where?
Check out the migration guide https://bloclibrary.dev/#/migration
It looks like your code is still version 7.1 or lower. You first need to follow the migration from 7.1 to 7.2. This will show you how to migrate your mapEventToState to the new on<Event> format.
How can I mock a function in flutter and verify it has been called n times?
Ive tried implementing Mock from mockito but it only throws errors:
class MockFunction extends Mock {
call() {}
}
test("onListen is called once when first listener is registered", () {
final onListen = MockFunction();
// Throws: Bad state: No method stub was called from within `when()`. Was a real method called, or perhaps an extension method?
when(onListen()).thenReturn(null);
bloc = EntityListBloc(onListen: onListen);
// If line with when call is removed this throws:
// Used on a non-mockito object
verify(onListen()).called(1);
});
});
As a workaround I am just manually tracking the calls:
test("...", () {
int calls = 0;
bloc = EntityListBloc(onListen: () => calls++);
// ...
expect(calls, equals(1));
});
So is there a way I can create simple mock functions for flutter tests?
What you could do is this:
class Functions {
void onListen() {}
}
class MockFunctions extends Mock implements Functions {}
void main() {
test("onListen is called once when first listener is registered", () {
final functions = MockFunctions();
when(functions.onListen()).thenReturn(null);
final bloc = EntityListBloc(onListen: functions.onListen);
verify(functions.onListen()).called(1);
});
}
The accepted answer is correct, but it doesn't represent a real-life scenario where you will probably want to substitute a top-level function with a mock or a fake. This article explains how to include top-level functions in your dependency injection composition so that you can substitute those functions with mocks.
You can compose dependency injection like this and point to top-level functions such as launchUrl with ioc_container.
IocContainerBuilder compose() => IocContainerBuilder(
allowOverrides: true,
)
..addSingletonService<LaunchUrl>(
(url, {mode, webOnlyWindowName, webViewConfiguration}) async =>
launchUrl(
url,
mode: mode ?? LaunchMode.platformDefault,
webViewConfiguration:
webViewConfiguration ?? const WebViewConfiguration(),
webOnlyWindowName: webOnlyWindowName,
),
)
..add((container) => MyApp(launchUrl: container<LaunchUrl>()));
Then, you can use the technique mentioned in the answer here to mock with Mocktail.
import 'package:fafsdfsdf/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/material.dart';
class LaunchMock extends Mock {
Future<bool> call(
Uri url, {
LaunchMode? mode,
WebViewConfiguration? webViewConfiguration,
String? webOnlyWindowName,
});
}
void main() {
testWidgets('Test Url Launch', (tester) async {
//These allow default values
registerFallbackValue(LaunchMode.platformDefault);
registerFallbackValue(const WebViewConfiguration());
//Create the mock
final mock = LaunchMock();
when(() => mock(
flutterDevUri,
mode: any(named: 'mode'),
webViewConfiguration: any(named: 'webViewConfiguration'),
webOnlyWindowName: any(named: 'webOnlyWindowName'),
)).thenAnswer((_) async => true);
final builder = compose()
//Replace the launch function with a mock
..addSingletonService<LaunchUrl>(mock);
await tester.pumpWidget(
builder.toContainer()<MyApp>(),
);
//Tap the icon
await tester.tap(
find.byIcon(Icons.favorite),
);
await tester.pumpAndSettle();
verify(() => mock(flutterDevUri)).called(1);
});
}
Something is wrong with this code, but I can not figure out what is it.
The problem is that loading is executed, and the Future finishes successfully and prints "From the onPress", but the data function is never executed so never prints "Done from the button!!!"
This is an example that reproduces the problem:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(ProviderScope(child: MaterialApp(home: Scaffold(body: Center(child: MyHomeAsyncValue()),))));
}
final voidFutureProvider = FutureProvider.autoDispose.family<void, String>((ref, str) {
return Future.delayed(Duration(seconds: 5), () { print(str); });
});
class MyHomeAsyncValue extends ConsumerWidget {
MyHomeAsyncValue({Key? key,}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
return ElevatedButton(
child: Text('Call'),
onPressed: () {
final AsyncValue<void> fromButton = context.read(voidFutureProvider("From the onPress"));
fromButton.when(
data: (value) => print("Done from the button!!!"),
loading: () => print("Loading...."),
error: (error, stackTrace) => print("Error $error"),
);
},
);
}
}
Note: I tried to wrap the button in a Consumer and use watch instead of read, but the result was the same.
Version flutter_riverpod: ^0.14.0+3
Updates:
Also tested avoiding void as the return type, with the same result:
final userUpdateFutureProvider = FutureProvider.autoDispose.family<User, User>((ref, user) async {
return Future.delayed(Duration(seconds: 100), () => user);
});
Update: Created a ticket https://github.com/rrousselGit/river_pod/issues/628
After creating a ticket thinking it is a bug, the author responded really quickly.
Basically, his response is:
This is not how AsyncValue works. when does not "listen" to
data/loading/error. It's a switch-case on the current state.
So replacing the onPressed code with the next new code works:
onPressed: () {
final value = context.read(voidFutureProvider("From the onPress").future);
value
.then((value) => print("Done from the button!!!"))
.onError((error, stackTrace) => print("Error $error"));
},
So the solution looks to use the inner future to register callbacks.
What is not clear for me is why does the same code work outside of the onPressed? 🤷
Here a full working example: https://github.com/angelcervera/testing_riverpod/blob/main/lib/main_onpress_working.dart
what is the correct approach to test riverpod with mockito?
running the code above,
/// ### edited snippets from production side ###
/// not important, skip to the TEST below!
/// this seems meaningless just because it is out of context
mixin FutureDelegate<T> {
Future<T> call();
}
/// delegate implementation
import '../../shared/delegate/future_delegate.dart';
const k_STRING_DELEGATE = StringDelegate();
class StringDelegate implements FutureDelegate<String> {
const StringDelegate();
#override
Future<String> call() async {
/// ... returns a string at some point, not important now
}
}
/// the future provider
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '<somewhere>/delegate.dart'; /// the code above
final stringProvider = FutureProvider<String>((ref) => k_STRING_DELEGATE());
/// ### edited snippets from TEST side ###
/// mocking the delegate
import 'package:mockito/mockito.dart';
import '<see above>/future_delegate.dart';
class MockDelegate extends Mock implements FutureDelegate<String> {}
/// actual test
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/all.dart';
import 'package:mockito/mockito.dart';
import '<somewhere in my project>/provider.dart';
import '../../domain/<somewhere>/mock_delegate.dart'; // <= the code above
void main() {
group('`stringProvider`', () {
final _delegate = MockDelegate();
test('WHEN `delegate` throws THEN `provider`return exception',
() async {
when(_delegate.call()).thenAnswer((_) async {
await Future.delayed(const Duration(seconds: 1));
throw 'ops';
});
final container = ProviderContainer(
overrides: [
stringProvider
.overrideWithProvider(FutureProvider((ref) => _delegate()))
],
);
expect(
container.read(stringProvider),
const AsyncValue<String>.loading(),
);
await Future<void>.value();
expect(container.read(stringProvider).data.value, [isA<Exception>()]);
});
});
}
running the test returns
NoSuchMethodError: The getter 'value' was called on null.
Receiver: null
Tried calling: value
dart:core Object.noSuchMethod
src/logic/path/provider_test.dart 28:48 main.<fn>.<fn>
I'm new to riverpod, clearly I'm missing something
I tried to follow this
I found that I had some extra errors specifically when using StateNotifierProvider. The trick was to not only override the StateNotifierProvider, but also its state property (which is a StateNotifierStateProvider object).
class SomeState {
final bool didTheThing;
SomeState({this.didTheThing = false});
}
class SomeStateNotifier extends StateNotifier<SomeState> {
SomeStateNotifier() : super(SomeState());
bool doSomething() {
state = SomeState(didTheThing: true);
return true;
}
}
final someStateProvider = StateNotifierProvider<SomeStateNotifier>((ref) {
return SomeStateNotifier();
});
class MockStateNotifier extends Mock implements SomeStateNotifier {}
void main() {
final mockStateNotifier = MockStateNotifier();
when(mockStateNotifier.doSomething()).thenReturn(true);
final dummyState = SomeState(didTheThing: true); // This could also be mocked
ProviderScope(
overrides: [
someStateProvider.overrideWithValue(mockStateProvider), // This covers usages like "useProvider(someStateProvider)"
someStateProvider.state.overrideWithValue(dummyState), // This covers usages like "useProvider(someStateProvider.state)"
],
child: MaterialApp(...),
);
}
There are 2 errors in your code
You're trying to test a throw error, so you should use thenThrow instead of thenAnswer, but because you're overriding a mixing method I would recommend instead of using Mock use Fake (from the same mockito library) to override methods and then throw it as you want
class MockDelegate extends Fake implements FutureDelegate<String> {
#override
Future<String> call() async {
throw NullThrownError; //now you can throw whatever you want
}
}
And the second problem (and the one your code is warning you) is that you deliberately are throwing, so you should expect an AsyncError instead, so calling container.read(stringProvider).data.value is an error because reading the riverpod documentation:
When calling data:
The current data, or null if in loading/error.
so if you're expecting an error (AsyncError) data is null, and because of that calling data.value its the same as writing null.value which is the error you're experiencing
This is the code you could try:
class MockDelegate extends Fake implements FutureDelegate<String> {
#override
Future<String> call() async {
throw NullThrownError;
}
}
void main() {
group('`stringProvider`', () {
final _delegate = MockDelegate();
test('WHEN `delegate` throws THEN `provider`return exception', () async {
final container = ProviderContainer(
overrides: [
stringProvider
.overrideWithProvider(FutureProvider((ref) => _delegate.call()))
],
);
expect(container.read(stringProvider), const AsyncValue<String>.loading());
container.read(stringProvider).data.value;
await Future<void>.value();
expect(container.read(stringProvider), isA<AsyncError>()); // you're expecting to be of type AsyncError because you're throwing
});
});
}
Also consider mocking out various providers by using an Override in your top level ProviderScope. That's what override can do quite well.
When I run my app normally, I execute cubit.getWeather('London') and the weatherLoading state emits, then weatherLoaded state emits CORRECTLY.
However when I run test for the cubit to test cubit.getWeather('London'), the weatherLoading state isn't emitted - it jumps straight to weatherLoaded state.
Why is this happening?
State:
#freezed
abstract class WeatherState with _$WeatherState {
factory WeatherState.weatherInitial() = WeatherInitial;
factory WeatherState.weatherLoading() = WeatherLoading;
factory WeatherState.weatherLoaded(Weather weather) = WeatherLoaded;
factory WeatherState.weatherError(String message) = WeatherError;
}
Cubit:
class WeatherCubit extends Cubit<WeatherState> {
final WeatherRepository _weatherRepository;
static Weather weather;
WeatherCubit(this._weatherRepository) : super(WeatherState.weatherInitial());
Future<void> getWeather(String cityName) async {
emit(WeatherState.weatherLoading());
try {
final weather = await _weatherRepository.fetchWeather(cityName);
emit(WeatherState.weatherLoaded(weather));
} on NetworkException {
emit(WeatherState.weatherError("Network error"));
}
}
}
My weather repository class has 'Future<Weather> fetchWeather(String cityname){...}' method.
And finally, my test:
class MockWeatherRepository extends Mock implements WeatherRepository {}
void main() {
MockWeatherRepository mockWeatherRepository;
WeatherCubit cubit;
final weather = Weather(cityName: 'London', temperatureCelsius: 7);
setUp(() {
mockWeatherRepository = MockWeatherRepository();
when(mockWeatherRepository.fetchWeather(any))
.thenAnswer((_) async => weather);
cubit = WeatherCubit(mockWeatherRepository);
});
test(
'emits [WeatherLoading, WeatherLoaded] when successful',
() async {
cubit.getWeather('London');
await expectLater(
cubit,
emitsInOrder([ //Fails
WeatherState.weatherLoading(),
WeatherState.weatherLoaded(weather),
]),
);
},
);
}
After a day of pause, I found a solution.
Put the expectLater before the cubit "act".
As i read from resoCoder as example of a bloc:
It is usually not be necessary to call expectLater before actually dispatching the event because it takes some time before a Stream emits its first value. I like to err on the safe side though.
I guess this is not true for cubits and you should always call expectLater before calling the method on the cubit.
test(
'emits [WeatherLoading, WeatherLoaded] when successful',
() {
expectLater(
cubit,
emitsInOrder([ //Fails
WeatherState.weatherLoading(),
WeatherState.weatherLoaded(weather),
]),
);
cubit.getWeather('London');
},
);
Alternative approach, from reading bloc_test:
You can use blocTest:
blocTest creates a new cubit-specific test case with the given
description. blocTest will handle asserting that the cubit emits the
expected states (in order) after act is executed. blocTest also
handles ensuring that no additional states are emitted by closing the
cubit stream before evaluating the expectation.
In my case just moving the test to bloctest worked like i expected the test to work.
blocTest('emits [WeatherLoading, WeatherLoaded] when successful',
build: () => WeatherCubit(mockWeatherRepository),
act: (WeatherCubit cubit) {
when(mockWeatherRepository.fetchWeather(any)).thenAnswer((_) => weather);
cubit.getWeather('London');
},
expect: [
WeatherState.weatherLoading(),
WeatherState.weatherLoaded(weather),
]);