Following the introduction of the BLoC pattern in the first post we will now see how we can use and test it.
Using Cubit/Bloc
To observe state changes in our Cubit/Bloc we can override the onChange
method.
If we want to react to state changes in our UI, we first need to provide the Cubit/Bloc to the UI using a BlocProvider
. This allows us to access the Cubit and its state in the subtree of our Widget tree.
@override Widget build(BuildContext context) { return BlocProvider( create: (context) => UserCubit(), child: Center( child: Padding( padding: const EdgeInsets.only(right: 5), child: child, ), ), ); }
After that we can use Widgets like BlocBuilder
and BlocListener
inside our view Widgets.
In our Paperchase app, for instance, we have a Widget that displays the number of points the user has gained so far.
When we call increasePoints,
the UserCubit
emits a new state with the updated number of points. The builder
function of our BlocBuilder
responds to these state changes and renders the UI with the updated number of points.
class AppBarPointsWidget extends StatelessWidget { const AppBarPointsWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return BlocBuilder(builder: (context, state) { return Text( state.points.toString(), style: TextStyle(fontSize: 25), ); }); } }
If you want to show a modal when the state changes, you can use BlocListener
.
BlocListener<BlocA, BlocAState>( listener: (context, state) { if(state == true){ // do something like show dialog }, child: Container(), )
Testing
Now that we have discussed what Blocs and Cubits are and how to use them, we will learn to test our logic.
For testing, we will use the Flutter packages mocktail and bloc_test.
Cubit tests
Let’s start by testing our business logic inside the Cubits. To do this, we will use the bloc_test package which provides us a blocTest
. In this blocTest
we can define:
seed
: the input state
build
: the Cubit instance under test
act
: the callback, which will be invoked with the Cubit under test
expect
: the expected state change
void main() { late AnimationState animationState; late AnimationCubit animationCubit; setUp(() { animationCubit = AnimationCubit(); animationState = animationCubit.state; }); group('When setCompassAnimation to true', () { blocTest( 'Then isCompassAnimation should be true', seed: () => state.copyWith(isCompassAnimated: false), build: () => animationCubit, act: (cubit) => animationCubit.setCompassAnimation(true), expect: () => [state.copyWith(isCompassAnimated: true)], ); }); });
“Dumb”-Widget tests
In this context, a “dumb”-Widget is a Widget that doesn’t use a Cubit or its state.
To tests these Widgets, we can use simple Widget tests.
In our example, we want to test a simple AppBarItemWidget
with a child Widget as parameter.
import 'package:flutter/material.dart'; class AppBarItemWidget extends StatelessWidget { final Widget child; const AppBarItemWidget({ required this.child, }); @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.only(right: 5), child: child, ), ); } }
In our test, we wrap the Widget with a MaterialApp
. This is necessary because our Widget has components that come from the Material part of Flutter UI components. That is why it needs a MaterialApp
as a parent.
After rendering the UI with pumpWidget
, we expect to find the text we provided to the Widget as a child.
void main() { testWidgets('Then should show Text', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp(home: AppBarItemWidget(child: Text('hello world')))); expect(find.text('hello world'), findsOneWidget); }); }
Widget tests
Lastly, we want to test a Widget that uses our Cubit methods and provided state.
Our example is the AppBarPointsWidget
, which can trigger the increasePoints()
method of our Cubit and renders a text which displays the current points state.
class AppBarPointsWidget extends StatelessWidget { @override Widget build(BuildContext context) { final userCubit = context.read(); return BlocBuilder(builder: (context, state) { return Column(children: [ Text( state.points.toString(), style: TextStyle(fontSize: 25), ), TextButton( onPressed: userCubit.increasePoints, child: Text('click me'), ) ]); }); } }
To test this Widget, we first need to mock our Cubit because we want to unit-test the Widget, which means we do not want to use the real Cubit. If we used the real Cubit, we would test the Widget as well as the Cubit, which would be more of an integration test instead of a unit test.
After creating the cubitMock
, we define the state our mock returns with the when()
method.
Now, we can simply render our Widget as we saw before and test to see if the rendered output is as expected.
testWidgets('Should show widget with mocked state', (WidgetTester tester) async { UserCubit cubitMock = MockUserCubit(); when(() => cubitMock.state) .thenReturn(UserState.initial().copyWith(points: 101)); await tester.pumpWidget( MaterialApp( home: BlocProvider.value( value: cubitMock, child: AppBarPointsWidget(), ), ), ); expect(find.text('101'), findsOneWidget); });
Additionally, we can test whether the Cubit method is called when we tap
the button.
To do this, we write another test that taps
a button and verifies
that the correct method is called.
testWidgets('Should call IncreasePoints', (WidgetTester tester) async { UserCubit cubitMock = MockUserCubit(); when(() => cubitMock.state) .thenReturn(UserState.initial().copyWith(points: 101)); await tester.pumpWidget( MaterialApp( home: BlocProvider.value( value: cubitMock, child: AppBarPointsWidget(), ), ), ); await tester.tap(find.text('click me')); verify(() => cubitMock.increasePoints()).called(1); });
Conclusion
As you can see, the BLoC pattern approach helps us create a maintainable architecture where we can easily unit test our business logic as well as our views.
A testable architecture is important, but the BLoC pattern is not the only way to achieve this goal. Depending on the technology and the project, there are a lot of other patterns and approaches you can use.
Regarding state management, in addition to BLoC, Flutter provides many other packages and approaches you can use in your project, like Riverpod or flutter_redux.
Redux, for example, is often used in web applications.
Patterns like MVVM or MVC are also widespread when it comes to software development. It is Important to use an architecture that is maintainable and testable.
- Using the BLoC-Pattern - 5. Februar 2024
- The BLoC-Pattern - 11. Januar 2024
- How to onboard - 1. Juli 2022