Hi, in my application I have implemented a navigation system witch swaps the correct widget when the user presses in a bottom navigation bar; in this way, every widget is considered a "page" and has his unique functionality.
At the moment every "page" widget internally uses a CubitProvider and a CubitBuilder to work on a Cubit (different for every page).
The problem comes when I write widget tests because I'm testing a single "page" and I need to mock his Cubit, but the provider is inside the widget and it can't be touched. The more simple way I think will be to create a constructor that takes a mock Cubit instance and provides it, but it's not the most beautiful thing and forces me to close the Cubit.
I've seen that a MultiCubitProvider exists and I was wondering if would be a more concise solution to "extract" the provider from inside the widgets and provide all of the Cubits at a higher level. This way testing the single page widget will be much more simple, but I don't know if the results will be propagated corrected, especially because in some Cubit I call a fetch() method right after his creation.
Does the Multi Cubit Provider idea make sense? Is there any other strategy that I can use?
Hi @Supercaly 馃憢
Thanks for opening an issue!
I would recommend separating your widget into two (one that is responsible for providing the cubit and one that is responsible for consuming the cubit).
For example rather than having something like:
class MyScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CubitProvider(
create: (_) => MyCubit(),
child: CubitBuilder<MyCubit, MyCubitState>(
builder: (context, state) { ... },
),
);
}
}
I would recommend decomposing the widget like:
class MyScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CubitProvider(
create: (_) => MyCubit(),
child: MyView(),
);
}
}
class MyView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CubitBuilder<MyCubit, MyCubitState>(
builder: (context, state) { ... },
);
}
}
With this separation you can easily write widget tests for the MyScreen widget that expect to find a MyView instance.
Then, you can go in-depth testing MyView with a MockCubit like:
class MockMyCubit extends MockCubit<MyCubitState> implements MyCubit {}
void main() {
group('MyView', () {
MyCubit myCubit;
setUp(() {
myCubit = MockMyCubit();
});
testWidgets('...', (tester) async {
when(myCubit.state).thenReturn(SomeState());
await tester.pumpWidget(
CubitProvider.value(
value: myCubit,
child: MyView(),
),
);
expect(...);
});
});
}
Hope that helps!
Closing for now but feel free to comment with additional questions and I'm happy to continue the conversation 馃憤
Hi, yesterday I've tested my idea of the MultiCubitProvider, but it didn't satisfy me, mainly because the cubits are initialized only one time, in fact emitting the initial state only one time, no matter how many times I call the fetch() method; because the initial state is a loading state if I re-visit the page a second time the loading animation is not displayed (because fetch never emits it). This problem can be fixed letting fetch emit that state.
Anyway, today I've implemented your suggestion, which I think is better, splitting the screen in two, but maintaining both the class in the same file, to avoid confusion in the source (having more that one file called home_*.dart). I will extend this idea marking the MyView class with @visibleForTesting so it can be used only by MyScreen.
The last thing: I've implemented the test as you suggested, but it gives me an error:
The following assertion was thrown building HomeView:
CubitProvider.of() called with a context that does not contain a Cubit of type HomeCubit.
No ancestor could be found starting from the context that was passed to
CubitProvider.of<HomeCubit>().
This can happen if the context you used comes from a widget above the CubitProvider.
The context used was: CubitBuilder<HomeCubit, HomeState>(dirty, state:
_CubitBuilderBaseState<HomeCubit, HomeState>#3067e(lifecycle state: created))
The test class:
void main() {
group("Test HomeView", () {
MockHomeCubit cubit;
setUp(() {
cubit = MockHomeCubit();
});
testWidgets("aaa", (tester) async {
await tester.pumpWidget(CubitProvider.value(
value: cubit,
child: HomeView(),
));
});
});
}
class MockHomeCubit extends MockCubit<HomeState> implements HomeCubit {}
The HomeScreen:
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Petify"),
actions: [
IconButton(
icon: Icon(Icons.filter_list),
onPressed: () {
showFilterBottomSheet(context);
},
)
],
),
body: CubitProvider<HomeCubit>(
create: (context) => HomeCubit()..fetch(),
child: HomeView(),
),
);
}
}
The HomeView:
class HomeView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CubitBuilder<HomeCubit, HomeState>(
builder: (context, state) {
print(state);
if (state is HomeStateEmpty)
return Center(child: Text("Empty"));
else if (state is HomeStateError)
return Center(child: Text("Some error occurred ${state.msg}"));
else if (state is HomeStateSuccess)
return ListView.builder(
itemCount: state.data.length,
itemBuilder: (context, index) =>
_buildHomeCard(context, state.data[index]),
);
else
return Center(child: Text("Loading..."));
},
);
}
}
After some fiddling around I solved the error in testing: the variable cubit should be declared of type HomeCubit and not MockHomeCubit, but still initialized as MockHomeCubit
Most helpful comment
After some fiddling around I solved the error in testing: the variable
cubitshould be declared of typeHomeCubitand notMockHomeCubit, but still initialized asMockHomeCubit