New to testing blocs in flutter. I can't get this simple test to work:
Here is the bloc class:
#injectable
class MyBloc extends Bloc<MyEvent, MyState> {
final MyUseCase _myUseCase;
MyBloc(this._myUseCase): super(const MyState()) {
on<LoadingEvent>(_onLoadingEvent);
}
FutureOr<void> _onLoadingEvent(
LoadingEvent event, Emitter<MyState> emit) async {
String email = await _myUseCase.fetchEmailAddress();
emit(state.copyWith(email: email, status: MyStatus.initial));
}
}
And the state class:
class MyState extends Equatable {
const MyState(
{this.email = "",
this.status = MyStatus.initial,
});
final String email;
final MyStatus status;
MyState copyWith(
{String? email,
MyStatus? status
}) {
return MyState(
email: email ?? this.email,
status: status ?? this.status,
);
}
}
And this is the test that is failing:
#GenerateMocks([MyUseCase])
void main() {
late MockMyUseCase mockMyUseCase;
late MyBloc bloc;
setUp(() {
mockMyCase = MockMyUseCase();
bloc = MyBloc(mockMyUseCase);
});
test('initial state', () async {
when(mockMyUseCase.fetchEmailAddress()).thenAnswer((_) async => "email");
bloc.add(const LoadingEvent());
await expectLater(
bloc.state,
emitsInOrder([
const MyState(email: "email", status: MyStatus.initial)
]));
});
}
What I am really trying to test for is the status that comes back. But here is the error:
Expected: should emit an event that MyState:<MyState(email, MyStatus.initial)>
Actual: MyState:<MyState(, MyStatus.initial)>
Which: was not a Stream or a StreamQueue
I'm really close, but I can't figure out what this error message is telling me.
Related
I have an authorization cubit and I want to test signIn request, but I can't find out how to do this..
This is my signIn request inside cubit:
class AuthCubit extends Cubit<AuthState> {
AuthCubit() : super(const AuthState(
email: "",
password: "",
));
final ApiService _apiService = ApiService();
void setEmail(String email) => emit(state.copyWith(email: email));
void setPassword(String password) => emit(state.copyWith(password: password));
Future signIn() async {
emit(WaitingSignInAuth(state.email, state.password));
try {
final data = await _apiService.signIn(
email: state.email ?? "",
password: state.password ?? ""
);
if (data['success']) {
emit(const SuccessAutentification());
} else {
emit(ErrorAuthentification(state.email, state.password,ex));
}
} on DioError catch(ex) {
emit(ErrorAuthentification(state.email, state.password,ex));
}
}
}
This is my state:
class AuthState extends Equatable {
final String? email;
final String? password;
const AuthState({this.email, this.password,});
AuthState copyWith({String? email, String? password}) { return AuthState(
email: email ?? this.email,
password: password ?? this.password,
);
}
#override
List<Object?> get props => [email, password];
}
And this is my blocTest():
class MockAuth extends MockBloc<AuthCubit, AuthState> implements AuthCubit {
}
void main() {
blocTest<AuthCubit, AuthState>(
"Test signIn",
build: AuthCubit(),
act: (cubit) async {
cubit.setEmail("sad#gmail.com");
cubit.setPassword("secret");
cubit.signIn(); // Here is only working WaitingSignInAuth but other statements not occur at all
}
),
expect: () => [
const AuthState(email: "sad#gmail.com", password: ""),
const AuthState(email: "sad#gmail.com", password: "secret"),
const WaitingSignInAuth("sad#gmail.com", "secret");
ErrorAuthentification("sad#gmail.com", "secret", '{statusCode: 400, message: incorrect password}');
];
}
I am trying to test flutter for the first time and I don't know how to do this properly. I tried to read documentations but it's so confusing for me.
When I call cubit.signI() I want to control the answer, like this =>
when(cubit.signIn()).thenAnswer((_) async => http.Response('{statusCode: 200, ...}'))
And then cubit.signIn() emit different statements depending on the response. But try/catch bloc doesn't even called when I make cubit.signIn()..
When not connected to the internet, executing the following code will cause a _ClientSocketException.
How should I write the exception handling?
class AsyncTodosNotifier extends AsyncNotifier<List<Todo>> {
Future<List<Todo>> _fetchTodo() async {
final json = await http.get('api/todos'); //** _ClientSocketException Error occurred**
final todos = jsonDecode(json) as List<Map<String, dynamic>>;
return todos.map((todo) => Todo.fromJson(todo)).toList();
}
#override
Future<List<Todo>> build() async {
return _fetchTodo();
}
Using the AsyncNotifier's build() from riverpod, I would like to code exception handling with AsyncValue.guard, but it results in a syntax error.
How should I write it to make it work?
When trying to get json data, if I can't connect to the internet, I want to write exception handling so that it doesn't abort.
Reference:
https://docs-v2.riverpod.dev/docs/providers/notifier_provider
full code:
implementation.
#immutable
class Todo {
const Todo({
required this.id,
required this.description,
required this.completed,
});
factory Todo.fromJson(Map<String, dynamic> map) {
return Todo(
id: map['id'] as String,
description: map['description'] as String,
completed: map['completed'] as bool,
);
}
final String id;
final String description;
final bool completed;
Map<String, dynamic> toJson() => <String, dynamic>{
'id': id,
'description': description,
'completed': completed,
};
}
class AsyncTodosNotifier extends AsyncNotifier<List<Todo>> {
Future<List<Todo>> _fetchTodo() async {
final json = await http.get('api/todos'); //** _ClientSocketException Error occurred**
final todos = jsonDecode(json) as List<Map<String, dynamic>>;
return todos.map((todo) => Todo.fromJson(todo)).toList();
}
#override
Future<List<Todo>> build() async {
return _fetchTodo();
}
Future<void> addTodo(Todo todo) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
await http.post('api/todos', todo.toJson());
return _fetchTodo();
});
}
}
final asyncTodosProvider =
AsyncNotifierProvider<AsyncTodosNotifier, List<Todo>>(() {
return AsyncTodosNotifier();
});
In constructor of the following bloc, I subscribe to a stream of firebase auth changes. go_router handles redirection.
When a user signs in/out _onAppUserChanged is called. If successfully signed in, I await for user's firestore data to be fetched. If there is, among the data, a location entry, I need to emit AppState with status==AppStatus.hasLocation if and only if my CatalogBlog has emitted state with status==CatalogStatus.loaded. Neither await for (var catalogState in _catalogBloc.stream) nor adding a listener will work. AppState with status: AppStatus.hasLocation won't be emmited although CatalogBloc will definatelly emit with status==CatalogStatus.loaded at some point.
class AppBloc extends Bloc<AppEvent, AppState> {
final UserRepository _userRepository;
final AuthRepository _authRepository;
final CatalogBloc _catalogBloc;
AppBloc(
UserRepository userRepository,
AuthRepository authRepository,
PrefsRepository prefsRepository,
CatalogBloc catalogBloc,
) : _userRepository = userRepository,
_authRepository = authRepository,
_catalogBloc = catalogBloc,
super(const AppState()) {
on<AppUserChanged>(_onAppUserChanged);
on<AppSignOutRequested>(_onAppSignOutRequested);
on<AppSelectedLocationSet>(_onAppSelectedLocationSet);
_authRepository.getAuthStateChanges().listen((String uid) {
add(AppUserChanged(uid));
});
}
void _onAppUserChanged(AppUserChanged event, Emitter<AppState> emit) async {
if (event.uid.isEmpty) {
emit(const AppState(status: AppStatus.noUser));
} else {
await for (var user in _userRepository.getUserData(event.uid)) {
if (_catalogBloc.state.status == CatalogStatus.initial) {
_catalogBloc.add(CatalogStarted());
}
if (user.locations.isEmpty) {
// Go to '/location_search'
emit(AppState(status: AppStatus.noLocation, user: user));
} else {
// Wait for catalog data to be fetched, then go to '/catalog'
await for (var catalogState in _catalogBloc.stream) {
if (catalogState.status == CatalogStatus.loaded) {
emit(AppState(
status: AppStatus.hasLocation,
user: user,
selectedLocation: user.prefLocation,
));
}
}
}
}
}
}
}
App State:
enum AppStatus { initial, hasLocation, noLocation, noUser, error }
class AppState extends Equatable {
final AppStatus status;
final User user;
final Location? selectedLocation;
const AppState({
this.status = AppStatus.initial,
this.user = const User(),
this.selectedLocation = const Location(),
});
#override
List<Object?> get props => [status, user, selectedLocation];
AppState copyWith({
AppStatus? status,
User? user,
Location? selectedLocation,
}) {
return AppState(
status: status ?? this.status,
user: user ?? this.user,
selectedLocation: selectedLocation ?? this.selectedLocation);
}
}
AppEvents:
abstract class AppEvent extends Equatable {
const AppEvent();
#override
List<Object> get props => [];
}
class AppUserChanged extends AppEvent {
final String uid;
const AppUserChanged(this.uid);
#override
List<Object> get props => [uid];
}
User repo:
class UserRepository {
final FirebaseFirestore _db = FirebaseFirestore.instance;
late User? user;
Stream<User> getUserData(String uid) {
var documentStream = _db.collection('users').doc(uid).snapshots();
return documentStream.map((snapshot) {
var data = snapshot.data();
if (data != null) {
user = User.fromJson(data);
return user!;
}
throw Exception("Firestore entry was not created for the signed in user");
});
}
...
}
I'm trying to make a unit test using the bloc_test library.
Here are my codes.
Login Cubit
class LoginCubit extends Cubit<LoginState> with HydratedMixin {
final UserRepository _userRepository;
LoginCubit(this._userRepository) : super(LoginInitial());
Future<void> login (String email, String password , bool remember) async {
bool result = await _userRepository.isLoginCorrectWithEmailAndPassword(email, password);
if (result){
emit(LoggedIn(remember: remember, email: email));
} else {
emit(LoginError());
}
}
}
Login States
part of 'login_cubit.dart';
#immutable
abstract class LoginState extends Equatable {}
class LoginInitial extends LoginState {
final bool remember;
final String email;
LoginInitial({this.remember = false, this.email = ''});
#override
List<Object?> get props => [remember, email];
}
class LoggedIn extends LoginState {
final bool remember;
final String email;
LoggedIn({required this.remember, required this.email});
#override
List<Object?> get props => [remember, email];
}
class LoginError extends LoginState {
LoginError();
#override
List<Object?> get props => [];
}
Unit Test
class MockUserRepository extends Mock implements UserRepository {
#override
Future<bool> isLoginCorrectWithEmailAndPassword(String email, String password) {
return Future.value(true);
}
}
void main() {
group('LoginCubit', () {
late LoginCubit loginCubit;
setUp(() {
loginCubit = LoginCubit(MockUserRepository());
});
tearDown(() {
loginCubit.close();
});
test('the initial state value is LoginInitial', () {
expect(loginCubit.state, LoginInitial());
});
blocTest<LoginCubit, LoginState>(
'TODO: description',
build: () => loginCubit,
act: (cubit) => cubit.login("any email", "any password", true),
expect: () => <LoginState>[
LoggedIn(remember: true, email: "any email"),
],
);
});
}
My issue is that the second test return always an empty array.
With some prints, I'm sure that the code is emitting the LoggedIn states but the test actually don't recognize it.
Where did I make a mistake ? :)
You forgot to add the stub for the repository function call.
Create an Object from the Mock Class MockUserRepository repo = MockUserRepository();
Pass this object to the LoginCubit in the setUp function like this
setUp(() { loginCubit = LoginCubit(repo); });
Add this line to the act function in the test
when(repo.isLoginCorrectWithEmailAndPassword(any, any)).thenAnswer((_) async => true);
so the whole test should be like this
blocTest<LoginCubit, LoginState>(
'TODO: description',
build: () => loginCubit,
act: (cubit) {
when(repo.isLoginCorrectWithEmailAndPassword(any, any)).thenAnswer((_) async => true);
cubit.login("any email", "any password", true);
},
expect: () => <LoginState>[
LoggedIn(remember: true, email: "any email"),
],
);
The issue was that I was using an Hydrated Bloc without initializing it in the test.
Here's the solution to it :
https://github.com/felangel/bloc/issues/2022#issuecomment-747918417
I'm testing my Flutter application and in particular the BLoC responsible of the logic behind the login form. I used the same code that can be found on the flutter_bloc library documentation examples (https://bloclibrary.dev/#/flutterfirebaselogintutorial).
This is the code for the LoginState:
part of 'login_bloc.dart';
/// Here is a list of the possible [LoginState] in which the [LoginForm] can be:
/// [empty]: initial state of the [LoginForm]
/// [loading]: state of the [LoginForm] when we are validating credentials
/// [failure]: state of the [LoginForm] when a login attempt has failed
/// [success]: state of the [LoginForm] when a login attempt has succeeded
class LoginState extends Equatable {
final bool isEmailValid;
final bool isPasswordValid;
final bool isSubmitting;
final bool isSuccess;
final bool isFailure;
bool get isFormValid => isEmailValid && isPasswordValid;
const LoginState({
#required this.isEmailValid,
#required this.isPasswordValid,
#required this.isSubmitting,
#required this.isSuccess,
#required this.isFailure,
});
factory LoginState.empty() {
return LoginState(
isEmailValid: true,
isPasswordValid: true,
isSubmitting: false,
isSuccess: false,
isFailure: false,
);
}
factory LoginState.loading() {
return LoginState(
isEmailValid: true,
isPasswordValid: true,
isSubmitting: true,
isSuccess: false,
isFailure: false,
);
}
factory LoginState.failure() {
return LoginState(
isEmailValid: true,
isPasswordValid: true,
isSubmitting: false,
isSuccess: false,
isFailure: true,
);
}
factory LoginState.success() {
return LoginState(
isEmailValid: true,
isPasswordValid: true,
isSubmitting: false,
isSuccess: true,
isFailure: false,
);
}
LoginState update({
bool isEmailValid,
bool isPasswordValid,
}) {
return copyWith(
isEmailValid: isEmailValid,
isPasswordValid: isPasswordValid,
isSubmitting: false,
isSuccess: false,
isFailure: false,
);
}
LoginState copyWith({
bool isEmailValid,
bool isPasswordValid,
bool isSubmitEnabled,
bool isSubmitting,
bool isSuccess,
bool isFailure,
}) {
return LoginState(
isEmailValid: isEmailValid ?? this.isEmailValid,
isPasswordValid: isPasswordValid ?? this.isPasswordValid,
isSubmitting: isSubmitting ?? this.isSubmitting,
isSuccess: isSuccess ?? this.isSuccess,
isFailure: isFailure ?? this.isFailure,
);
}
#override
List<Object> get props => [
isEmailValid,
isPasswordValid,
isSubmitting,
isSuccess,
isFailure,
];
#override
String toString() {
return '''
LoginState {
isEmailValid: $isEmailValid,
isPasswordValid: $isPasswordValid,
isSubmitting: $isSubmitting,
isSuccess: $isSuccess,
isFailure: $isFailure,
}''';
}
}
This is the code for the LoginEvent:
part of 'login_bloc.dart';
/// List of [LoginEvent] objects to which our [LoginBloc] will be reacting to:
/// [EmailChanged] - notifies the BLoC that the user has changed the email.
/// [PasswordChanged] - notifies the BLoC that the user has changed the password.
/// [Submitted] - notifies the BLoC that the user has submitted the form.
/// [LoginWithGooglePressed] - notifies the BLoC that the user has pressed the Google Sign In button.
/// [LoginWithCredentialsPressed] - notifies the BLoC that the user has pressed the regular sign in button.
abstract class LoginEvent extends Equatable {
const LoginEvent();
#override
List<Object> get props => [];
}
class EmailChanged extends LoginEvent {
final String email;
const EmailChanged({#required this.email});
#override
List<Object> get props => [email];
#override
String toString() => 'EmailChanged { email :$email }';
}
class PasswordChanged extends LoginEvent {
final String password;
const PasswordChanged({#required this.password});
#override
List<Object> get props => [password];
#override
String toString() => 'PasswordChanged { password: $password }';
}
class Submitted extends LoginEvent {
final String email;
final String password;
const Submitted({
#required this.email,
#required this.password,
});
#override
List<Object> get props => [email, password];
#override
String toString() => 'Submitted { email: $email, password: $password }';
}
class LoginWithGooglePressed extends LoginEvent {}
class LoginWithCredentialsPressed extends LoginEvent {
final String email;
final String password;
const LoginWithCredentialsPressed({
#required this.email,
#required this.password,
});
#override
List<Object> get props => [email, password];
#override
String toString() =>
'LoginWithCredentialsPressed { email: $email, password: $password }';
}
And this is the code for the LoginBloc:
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:rxdart/rxdart.dart';
import '../../../utils/validators.dart';
import '../../repositories/authentication/authentication_repository.dart';
part 'login_event.dart';
part 'login_state.dart';
/// BLoC responsible for the business logic behind the login process. In particular this BLoC will
/// map the incoming [LoginEvent] to the correct [LoginState].
class LoginBloc extends Bloc<LoginEvent, LoginState> {
/// Authentication repository that provides to the user the methods to sign-in
/// with credentials and to sign-in with a Google account.
final AuthenticationRepository authRepository;
LoginBloc({#required this.authRepository}) : assert(authRepository != null);
#override
LoginState get initialState => LoginState.empty();
// Overriding transformEvents in order to debounce the EmailChanged and PasswordChanged events
// so that we give the user some time to stop typing before validating the input.
#override
Stream<Transition<LoginEvent, LoginState>> transformEvents(
Stream<LoginEvent> events,
TransitionFunction<LoginEvent, LoginState> transitionFn,
) {
final nonDebounceStream = events.where((event) {
return (event is! EmailChanged && event is! PasswordChanged);
});
final debounceStream = events.where((event) {
return (event is EmailChanged || event is PasswordChanged);
}).debounceTime(Duration(milliseconds: 300));
return super.transformEvents(
nonDebounceStream.mergeWith([debounceStream]),
transitionFn,
);
}
#override
Stream<LoginState> mapEventToState(LoginEvent event) async* {
if (event is EmailChanged) {
yield* _mapEmailChangedToState(event.email);
} else if (event is PasswordChanged) {
yield* _mapPasswordChangedToState(event.password);
} else if (event is LoginWithGooglePressed) {
yield* _mapLoginWithGooglePressedToState();
} else if (event is LoginWithCredentialsPressed) {
yield* _mapLoginWithCredentialsPressedToState(
email: event.email,
password: event.password,
);
}
}
Stream<LoginState> _mapEmailChangedToState(String email) async* {
yield state.update(
isEmailValid: Validators.isValidEmail(email),
);
}
Stream<LoginState> _mapPasswordChangedToState(String password) async* {
yield state.update(
isPasswordValid: Validators.isValidPassword(password),
);
}
Stream<LoginState> _mapLoginWithGooglePressedToState() async* {
try {
await authRepository.signInWithGoogle();
yield LoginState.success();
} catch (_) {
yield LoginState.failure();
}
}
Stream<LoginState> _mapLoginWithCredentialsPressedToState({
String email,
String password,
}) async* {
yield LoginState.loading();
try {
await authRepository.signInWithCredentials(
email: email,
password: password,
);
yield LoginState.success();
} catch (_) {
yield LoginState.failure();
}
}
}
Now I'm trying to test this bloc using the bloc_test library, and in particular I'm testing the EmailChanged. As you can see from the LoginBloc code I added a debounce time of 300 milliseconds before mapping this event to the correct state.
For testing this event I used this code:
import 'package:covtrack/business/blocs/login/login_bloc.dart';
import 'package:covtrack/business/repositories/authentication/authentication_repository.dart';
import 'package:covtrack/utils/validators.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:bloc_test/bloc_test.dart';
class MockAuthenticationRepository extends Mock
implements AuthenticationRepository {}
void main() {
group('LoginBloc', () {
AuthenticationRepository authRepository;
LoginBloc loginBloc;
String email;
setUp(() {
authRepository = MockAuthenticationRepository();
loginBloc = LoginBloc(authRepository: authRepository);
email = 'johndoe#mail.com';
});
test('throws AssertionError if AuthenticationRepository is null', () {
expect(
() => LoginBloc(authRepository: null),
throwsA(isAssertionError),
);
});
test('initial state is LoginState.empty()', () {
expect(loginBloc.initialState, LoginState.empty());
});
group('EmailChanged', () {
blocTest(
'emits [LoginState] with isEmailValid true',
build: () async => loginBloc,
act: (bloc) async => bloc.add(EmailChanged(email: email)),
wait: const Duration(milliseconds: 300),
expect: [LoginState.empty().update(isEmailValid: true)],
);
});
});
}
When I run the test I get this error:
✓ LoginBloc throws AssertionError if AuthenticationRepository is null
✓ LoginBloc initial state is LoginState.empty()
Expected: [
LoginState: LoginState {
isEmailValid: true,
isPasswordValid: true,
isSubmitting: false,
isSuccess: false,
isFailure: false,
}
]
Actual: []
Which: shorter than expected at location [0]
package:test_api expect
package:bloc_test/src/bloc_test.dart 143:29 blocTest.<fn>.<fn>
===== asynchronous gap ===========================
dart:async _asyncThenWrapperHelper
package:bloc_test/src/bloc_test.dart blocTest.<fn>.<fn>
dart:async runZoned
package:bloc_test/src/bloc_test.dart 135:11 blocTest.<fn>
✖ LoginBloc EmailChanged emits [LoginState] with isEmailValid true
I don't understand the reason why no state at all is emitted.