I'm passing the CartBloc instance to the BlocProvider. But the test is failing as the ListView widget is not found.
Below is the test code.
void main() {
late MockCartRepository cartRepository;
late MockSharedPreferenceService sharedPreferenceService;
late AuthBloc authBloc;
late MockUserRepository userRepository;
late CartData cartData;
late CartBloc cartBloc;
setUp(() {
cartData = CartData.fromJson(guestCartData);
cartRepository = MockCartRepository();
sharedPreferenceService = MockSharedPreferenceService();
userRepository = MockUserRepository();
authBloc = AuthBloc(
service: sharedPreferenceService,
userRepository: userRepository,
);
cartBloc = CartBloc(
cartRepository: cartRepository,
sharedPreferenceService: sharedPreferenceService,
authBloc: authBloc,
);
});
_pumpTestWidgetWithEvent(WidgetTester tester, CartEvent event) {
return MaterialApp(
home: BlocProvider(
create: (_) => cartBloc..add(event),
child: const CartView(),
),
);
}
group('Testing cart view', () {
testWidgets(
'Adding item to cart',
(WidgetTester tester) async {
when(cartBloc.cartRepository.addToCart(
cartId: cartData.id!,
quantity: 1,
variantId: cartData.lineItems!.physicalItems!.first.variantId!,
productId: cartData.lineItems!.physicalItems!.first.productId!,
)).thenAnswer((_) async => cartData);
when(sharedPreferenceService.setString('cartId', cartData.id!))
.thenAnswer((_) async => true);
await tester.pumpWidget(
_pumpTestWidgetWithEvent(
tester,
AddToCartEvent(
cartId: cartData.id!,
quantity: 1,
variantId: cartData.lineItems!.physicalItems!.first.variantId!,
productId: cartData.lineItems!.physicalItems!.first.productId!,
),
),
);
await tester.pumpAndSettle();
expect(find.text('Cart'), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
},
);
});
}
The test passes when I pass a new instance of cart bloc as shown below.
_pumpTestWidgetWithEvent(WidgetTester tester, CartEvent event) {
return MaterialApp(
home: BlocProvider(
create: (_) => CartBloc(
cartRepository: cartRepository,
sharedPreferenceService: sharedPreferenceService,
authBloc: authBloc,
)..add(event),
child: const CartView(),
),
);
}
Why is this happening?
I've also tried using BlocProvider.value and passing cartBloc as value. But this also failing.
Related
I am trying to Widget test my WelcomeScreen(). WelcomeScreen has a BlocProvider and a BlocBuilder. After I load WelcomeBloc() it checks with an if statement inside the builder to check if the state is WelcomeLoadSuccessState.
How do I find something under the if statement if the statement is true?
My Welcome screen:
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => WelcomeBloc(),
child: BlocBuilder<WelcomeBloc, WelcomeState>(
builder: (context, state) {
if (state is WelcomeLoadSuccessState) {
return Scaffold(
body: Container(
child: Column(
children: [
Wrap(
direction: Axis.vertical,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Padding(
padding: EdgeInsets.all(8),
child: ShowUp(
delay: _delay + 200,
child: Text('Welcome user’, // <——— I want to find this one
)),
),
],
),
],
)),
);
}
// return LoadingWidget();
return Text('Something'); // <——— This one I can find
},
),
);
}
The test that I have now:
main() {
WelcomeBloc welcomeBloc;
WelcomeService welcomeService;
final Brand brand = Brand();
setUp(() {
setUpMocks();
welcomeService = localServices<WelcomeService>();
welcomeBloc = MockWelcomeBloc();
});
_createWidget(WidgetTester tester) async {
when(welcomeService.getBrand(id: '609a88d324a01928242d1ca9')).thenAnswer((realInvocation) => Future.value(brand));
welcomeBloc.add(WelcomeLoadRequestEvent(id: '609a88d324a01928242d1ca9'));
when(welcomeBloc.state).thenAnswer((_) => WelcomeLoadSuccessState(brand: brand));
print(welcomeBloc.state); //Correct State (WelcomeLoadSuccessState)
await tester.pumpWidget(
MaterialApp(
title: 'Flutter Demo',
home: WelcomeScreen(),
)
);
await tester.pump();
}
testWidgets('Welcome Screen Test', (WidgetTester tester) async {
await _createWidget(tester);
await tester.pump();
//expect(find.textContaining('Welcome user'), findsOneWidget); //What I want
expect(find.text('Something'), findsOneWidget); //This works
});
tearDown(() {
welcomeBloc?.close();
});
}
Thank you for helping.
I solved it:
change:
create: (context) => WelcomeBloc()
to:
create: (context) => WelcomeBloc()..add(WelcomeLoadRequestEvent(id: '609a88d324a01928242d1ca9')),
and my test is now this:
main() {
WelcomeBloc welcomeBloc;
WelcomeService welcomeService;
final Brand brand = Brand();
setUp(() {
setUpMocks();
welcomeService = localServices<WelcomeService>();
welcomeBloc = MockWelcomeBloc();
});
_createWidget(WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
title: 'Flutter Demo',
home: WelcomeScreen(),
));
await tester.pump(Duration(seconds: 10));
}
testWidgets('Welcome Screen Test', (WidgetTester tester) async {
when(welcomeService.getBrand(id: '609a88d324a01928242d1ca9'))
.thenAnswer((realInvocation) => Future.value(brand));
whenListen(
welcomeBloc,
Stream.fromIterable([
WelcomeLoadInProgressState(),
WelcomeLoadSuccessState(brand: brand),
]));
await _createWidget(tester);
await tester.pump(Duration(seconds: 5));
expect(find.textContaining('Welcome user'), findsOneWidget);
});
tearDown(() {
welcomeBloc?.close();
unRegister();
});
}
Edit to add:
For my other pages it was useful to separate the blocProvider and the blocBuilder. This way I was able to Mock my blocProvider with a MockMyBloc() and then give the screen in the child.
My real widgets:
MyWidgetMakeBlocProviders(
Widget build(context) {
return BlocProvider<MyBloc>(
create: (context) => MyBloc(),
child: MyScreen(),
);
}
)
MyScreen(
Widget build(context) {
return BlocBuilder<MyBloc, MyBlocState>(
builder: (context, state) {...}
);
}
)
My test:
testWidgets('', (tester) async {
whenListen(MockMyBloc, Stream.fromIterable([
InitState(),
LoadedState()
]));
await _createWidget(tester);
await tester.pump();
//expect()
});
_createWidget(tester) async {
await tester.pumpWidget(
MaterialApp(
title: '',
home: BlocProvider<MockMyBloc>(
create: (context) => MockMyBloc(),
child: MyScreen(),
)
)
);
await tester.pump();
}
I am having trouble testing my Riverpod FutureProvider when it returns an AsyncValue.error.
I tried the test as shown below:
// Future provider
final ipsProvider = FutureProvider.autoDispose((_) => IpRepository().fetchIps());
// Widget to be tested
class ExampleWidget extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
// hooks
AsyncValue<List<Ip>> ips = watch(ipsProvider);
return Container(
child: ips.when(
data: (data) => randomWidget,
loading: () => progressIndicator,
error: (_, stack) => Text('YesMan'),
),
);
}
}
// Test: I am trying to find a Text widget containing message 'YesMan'
testWidgets('ExampleWidget | error', (WidgetTester tester) async
await tester.pumpWidget(
ProviderScope(
overrides: [
ipsProvider.overrideWithValue(AsyncValue.error('randomErrorMessage')),
],
child: MaterialApp(
home: Builder(builder: (context) {
return ExampleWidget();
}),
),
),
);
final finderError = find.text('YesMan');
expect(finderError, findsOneWidget);
});
I expected the test to return a text widget with message 'randomError', but instead it throws an exception as below:
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK The following message was thrown running a test: randomErrorMessage When the exception was thrown, this was the stack:
Any idea on how to test AsyncValue.error cases in Riverpod?
Thank you
instead of overrideWithValue try overrideProvider and run a future.error so FutureProvider catch it
class ExampleWidget extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
// hooks
AsyncValue<String> ips = watch(ipsProvider);
return Container(
child: ips.when(
data: (data) => SizedBox(),
loading: () => CircularProgressIndicator(),
error: (_, stack) => Text('YesMan'),
),
);
}
}
testWidgets('ExampleWidget | error', (WidgetTester tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
ipsProvider.overrideWithProvider(FutureProvider((_) => Future.error('error'))),
],
child: MaterialApp(
home: Builder(builder: (context) {
return ExampleWidget();
}),
),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
await tester.pump();
final finderError = find.text('YesMan');
expect(finderError, findsOneWidget);
});
My HomePagewhere Providers are initilized:
Widget build(BuildContext context) {
return SafeArea(
child: MultiProvider(
providers: [
ChangeNotifierProvider<EmailAuth>(create: (context) => EmailAuth()),
],
child: Scaffold(
resizeToAvoidBottomInset: true,
floatingActionButton: FloatingActionButton(.....
My Authentication function that is triggered when user logs-in (Firebase)
class EmailAuth extends ChangeNotifier {
final _auth = FirebaseAuth.instance;
final dbRef = FirebaseFirestore.instance.collection("users");
String userid;
Future signIn({String email, String password}) async {
final currentUser = await _auth.signInWithEmailAndPassword(
email: email, password: password);
if (currentUser != null) {
userid = _auth.currentUser.uid;
dbRef.doc(_auth.currentUser.uid).update({
"lastLogin": DateTime.now(),
});
} else {
print("something didn't work");
}
print(userid);
notifyListeners();
return userid;
}
}
This is how my Consumer is setup in the HomePage - AppBar
title: Consumer<EmailAuth>(
builder: (context, data, child) => Text(
"${data.userid}",
style: TextStyle(color: Colors.indigoAccent),
),
),
But the output on AppBar is NULL. What am I doing wrong?!
I have been using this as reference for implementation:
https://medium.com/flutter-community/making-sense-all-of-those-flutter-providers-e842e18f45dd
Something similar was a known error in the older Provider Package. Please update to latest and check if the issue is still there. However,
This is how a MultiProvider should look like:
#override
Widget build(BuildContext context) {
return MultiProvider( // <--- MultiProvider
providers: [
ChangeNotifierProvider<MyModel>(create: (context) => MyModel()),
ChangeNotifierProvider<AnotherModel>(create: (context) => AnotherModel()),
],
And should be consumed like this
child: Consumer<MyModel>( // <--- MyModel Consumer
builder: (context, myModel, child) {
return RaisedButton(
child: Text('Do something'),
onPressed: (){
// We have access to the model.
myModel.doSomething();
},
);
},
)
class MyModel with ChangeNotifier { // <--- MyModel
String someValue = 'Hello';
void doSomething() {
someValue = 'Goodbye';
print(someValue);
notifyListeners();
}
}
I'm trying to create widget tests for a flutter application using GraphQL.
What I want to do is to test the behaviour of the app which depends on the result of a GraphQL Mutation on a user action.
This is a very simple example of the app:
class FirstScreen extends StatelessWidget {
#override
Widget return Container(
child: Mutation(
options: myMutationOptions,
onCompleted: (dynamic result) {
final bool myBool = result['bool'] as bool;
if (myBool) {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => SecondScreen()));
} else {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => ThirdScreen()));
}
},
builder: (RunMutation runMutation, QueryResult queryResult) {
return FlatButton(
child: Text('Button'),
onPressed: () async {
await runMutation(myParameters).networkResult;
},
);
},
),
);
}
What I would like to do is to mock the result of the mutation so in my widget tests, I can test that the button redirects to the SecondScreen or ThirdScreen depending of the result myBool.
How can I do that ?
I finally managed to successfully mock a GraphQL Mutation. Here is how I did it, it is inspired from #Gpack's comment but I had to add some modifications and details to it.
To make it easy to use I created a wrapper widget GraphQLMutationMocker :
class MockClient extends Mock implements Client {
MockClient({
this.mockedResult,
this.mockedStatus = 200,
});
final Map<String, dynamic> mockedResult;
final int mockedStatus;
#override
Future<StreamedResponse> send(BaseRequest request) {
return Future<StreamedResponse>.value(
StreamedResponse(
Stream.value(utf8.encode(jsonEncode(mockedResult))),
mockedStatus,
),
);
}
}
class GraphQLMutationMocker extends StatelessWidget {
const GraphQLMutationMocker({
#required this.child,
this.mockedResult = const {},
this.mockedStatus = 200,
this.url = 'http://url',
this.storagePrefix = 'test',
});
final Widget child;
final Map<String, dynamic> mockedResult;
final int mockedStatus;
final String url;
final String storagePrefix;
#override
Widget build(BuildContext context) {
final mockClient = MockClient(
mockedResult: mockedResult,
mockedStatus: mockedStatus,
);
final httpLink = HttpLink(
uri: url,
httpClient: mockClient,
);
final graphQLClient = ValueNotifier(
GraphQLClient(
cache: InMemoryCache(storagePrefix: storagePrefix),
link: httpLink,
),
);
return GraphQLProvider(
client: graphQLClient,
child: child,
);
}
}
Then it was pretty easy to write the tests
group('Test mutation', () {
testWidgets('It should redirect to SecondScreen', (WidgetTester tester) async {
await tester.pumpWidget(GraphQLMutationMocker(
mockedResult: <String, dynamic>{
'data': {
'bool': true,
},
},
child: FirstScreen(),
));
// Click on button
await tester.tap(find.text('Button'));
await tester.pumpAndSettle();
// Check I'm on the right screen
expect(find.byType(SecondScreen), findsOneWidget);
expect(find.byType(ThirdScreen), findsNothing);
});
testWidgets('It should redirect to ThirdScreen', (WidgetTester tester) async {
await tester.pumpWidget(GraphQLMutationMocker(
mockedResult: <String, dynamic>{
'data': {
'bool': false,
},
},
child: FirstScreen(),
));
// Click on button
await tester.tap(find.text('Button'));
await tester.pumpAndSettle();
// Check I'm on the right screen
expect(find.byType(SecondScreen), findsNothing);
expect(find.byType(ThirdScreen), findsOneWidget);
});
})
Create a mock of http.Client like in the flutter docs
In your test, wrap your FirstScreen in a GraphqlProvider like so:
class MockHttpClient extends Mock implements Client {}
group('Test mutation', () {
MockHttpClient mockHttpClient;
HttpLink httpLink;
ValueNotifier<GraphQLClient> client;
setUp(() async {
mockHttpClient = MockHttpClient();
httpLink = HttpLink(
uri: 'https://unused/graphql',
httpClient: mockHttpClient,
);
client = ValueNotifier(
GraphQLClient(
cache: InMemoryCache(storagePrefix: 'test'),
link: httpLink,
),
);
});
testWidgets('redirects to SecondScreen', (WidgetTester tester) async {
when(client.send(captureAny)).thenAnswer(/* ... */);
await tester.pumpWidget(GraphQLProvider(
client: client,
child: FirstScreen(),
));
// Click on button
verify(mockHttpClient.send(any)).called(1);
// etc.
});
})
I know flutter provider is scoped. So I declared providers (those will be needed everywhere) top of MaterialApp. In a screen am chaning a provider value and navigating to another screen. In that screen am not getting the data. Need suggestions and guide where I have done the mistake
main.dart
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<UserAuthViewModel>(
create: (context) => sl<UserAuthViewModel>()),
ChangeNotifierProvider<UserProfileViewModel>(
create: (context) => sl<UserProfileViewModel>()),
ChangeNotifierProvider<BottomNavViewModel>(
create: (context) => sl<BottomNavViewModel>()),
],
child: MaterialApp(
title: "Footsapp",
theme: ThemeData(fontFamily: 'Montserrat'),
debugShowCheckedModeBanner: false,
home: isFirstLaunch == true ? OnBoarding() : SplashView(),
routes: {
//onboarding
SplashView.SCREEN_ID: (context) => SplashView(),
//bottom nav pages
BottomNavContainer.SCREEN_ID: (context) => BottomNavContainer(),
},
),
),
);
splash screen where getting some info from api call
class _SplashViewState extends State<SplashView>
with TokenProvider, AfterLayoutMixin<SplashView> {
final userProfileViewModel = sl<UserProfileViewModel>();
final prefUtil = sl<SharedPrefUtil>();
#override
void afterFirstLayout(BuildContext context) {
Future.delayed(Duration(seconds: 1), () async {
bool isLoggedIn = prefUtil.readBool(IS_LOGGED_IN) ?? false;
bool initialProfileUpdated =
prefUtil.readBool(INITIAL_PROFILE_UPDATED) ?? false;
isLoggedIn == true
? getProfileInfo()
: await Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (_) => SocialLoginRegScreen(),
),
(route) => false);
});
}
#override
void initState() {
super.initState();
}
void getProfileInfo() async {
final userProfileResponse = await userProfileViewModel.getUserProfileData();
if (userProfileResponse.success == true) {
print('In splash: ${userProfileViewModel.userProfile.toString()}');
//from log
In splash: {firstName: Ashif, lastName: 123, birthday: 1990-02-03, email:
ashif123#gmail.com, preferredFoot: Left, preferredPosition: Midfielder,
numberOfTimesPlayedWeekly: 4}
Future.delayed(Duration(milliseconds: 500), () async {
await Navigator.pushReplacementNamed(
context,
BottomNavContainer.SCREEN_ID,
);
});
}
}
provider model class
class UserProfileViewModel extends BaseViewModel {
final _profileManageRepo = sl<ProfileManageRepo>();
Profile userProfile = Profile.initial();
Future<UserDataResponse> getUserProfileData() async {
final _userDataResponse = await _profileManageRepo.getUserProfileInfo();
if (_userDataResponse.success == true) {
userProfile = _userDataResponse.profile;
} else {
setState(ViewState.Error);
errorMessage = 'Please try again!';
}
return _userDataResponse;
}
Am trying to get the provider data (user profile) from Profile Screen. But it always get initial value.
class _UserProfileScreenState extends State<UserProfileScreen> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Consumer<UserProfileViewModel>(
builder: (context, viewmodel, child) {
print('In profile: ${viewmodel.userProfile.toString()}');
//but here
In profile: {firstName: , lastName: , birthday: , email: , preferredFoot:
Left, preferredPosition: , numberOfTimesPlayedWeekly: 1(default value)}
return Container(
child: ProfileCardWidget(
profile: viewmodel.userProfile,
),
);
},
);
}
}