Welcome back, Glad that you are here.
Flutter app development has been very easy to learn for beginners and supporting apps for multiple platforms has become easier. Flutter has been very common among college students as it is new and promising. Flutter also quickly helps with prototyping an app. Writing test cases is not a regular practice that many of you consider while creating a prototype, but writing unit test cases is very important.
Let's consider an example of MovieApp, where we have completed all the features and are now very much ready for putting it in production. But, we have not written any test cases for the app, which means that if say after releasing this app to production and making the code open-source on GitHub will allow any developer to raise an MR to the repo.
If you decide to modify the app years after the launch, you might mess that up and some features may not behave as expected. It doesn't matter how much confident are you about your changes, you might have missed one small thing. To avoid those hiccups, make unit test cases with your friend which will warn you before making mistakes.
Open the pubspec.yml file add these dependencies
bloc_test: ^7.1.0
mockito: ^4.1.2
flutter_test:
sdk: flutter
Scope of Bloc Test
In this tutorial, we are going to test only the bloc. I will demonstrate to you about MovieTabbedCubit
. Bloc and Cubit are the same so writing test cases for bloc are the same as writing test cases for a cubit.
The MovieTabbedCubit
takes three usecases and whenever there is a MovieTabChangedEvent
we will emit the MovieTabLoading
state, then we process based on the types of movies we want to bring from the API and, then we emit either MovieTabError
state or MovieTabChanged
state. This is how MovieTabbedCubit
works as of today.
Location of Test File
In the app you have lib -> presentation -> blocs -> movie_tabbed_bloc. Similarly, in the test folder you will create presentation -> blocs -> movie_tabbed folder where you will create a new file movie_tab_cubit_test.dart.
Using Mocks
First of all, we will try to mock everything which is used in this bloc. The MovieTabbedCubit
has three dependencies that should be mocked.
Mock Usecase
Add the mock for usecases
class GetPopularMock extends Mock implements GetPopular {}
class GetPlayingNowMock extends Mock implements GetPlayingNow {}
class GetComingSoonMock extends Mock implements GetComingSoon {}
Create a mock for the usecase which extends mock and implements the GetPopular
usecase. Similarly, create 2 more mock usecases GetPlayingNowMock
and GetComingSoonMock
. Append Mock
after type usecase's class name which will indicate that these classes are mock classes.
Main Method
The main
function will help us in running all the test cases which are in the test file. Declare these mock classes in the main method.
GetPopularMock getPopularMock;
GetPlayingNowMock getPlayingNowMock;
GetComingSoonMock getComingSoonMock;
Next, Declare the real instance of MovieTabbedCubit
because we want to execute all the methods that are defined in this cubit with this instance.
MovieTabbedCubit movieTabbedCubit;
To call methods in the cubit we will not use a mock this time.
SetUp
The setup method needs a function to execute before running all the test cases.
setUp(() {
//1
getPopularMock = GetPopularMock();
getPlayingNowMock = GetPlayingNowMock();
getComingSoonMock = GetComingSoonMock();
//2
movieTabbedCubit = MovieTabbedCubit(
getPopular: getPopularMock,
getPlayingNow: getPlayingNowMock,
getComingSoon: getComingSoonMock,
);
});
- Initialize the mocks.
GetPopularMock
,GetPlayingNowMock
andGetComingSoonMock
. - Initialize the bloc
MovieTabbedCubit
for which we want to run the test cases. Pass the three mocks that are required by cubit.
Tear Down
The tear-down method executes after all the test cases have run. Here we will close all the blocs that are initialized in setUp()
to free up the memory.
tearDown(() {
movieTabbedCubit.close();
});
Test for Initial State
We will test that our initial state is always MovieTabbedInitial
, so when if in the future somebody changing your initial state to something else this test case will fail. Write the description and an expectation for this test.
test('bloc should have initial state as [MovieTabbedInitial]', () {
expect(movieTabbedCubit.state.runtimeType, MovieTabbedInitial);
});
Run the test case for this file by running the below command:
fvm flutter test test/presentation/blocs/movie_tabbed/movie_tabbed_cubit_test.dart
I have used it for fvm (flutter version management) but you can directly write flutter test
.
Test for Tab Change
Let's create more functional test cases in the bloc now. We have blocTest()
that is written in the flutter bloc library.
blocTest(
'should emit [MovieTabLoading, MovieTabChanged] state when playing now tab changed success',
build: ,
act: ,
expect: ,
verify: ,
);
We have the description of the test and multiple attributes. Here's what happens when the movie tab is changed.
- Emit the
MovieTabLoading
state. - The movie is returned as a success.
- Emit the
MovieTabChanged
state.
MovieTabLoading
and MovieTabChanged
are the states that it will emit. Hence, our description becomes should emit [MovieTabLoading, MovieTabChanged] state when playing now tab changed success.
build
The build
attribute takes a function where it provides the instance of the cubit to this blocTest
.
build: () => movieTabbedCubit
act
The act
attribute takes in the bloc
which we provided in the build
and executes some statements when we are running the test case.
act: (MovieTabbedCubit cubit) {
//1
when(getPlayingNowMock.call(NoParams()))
.thenAnswer((_) async => Right([]));
//2
cubit.movieTabChanged(currentTabIndex: 1);
}
- When
getPlayingMock
is executed in thecubit
, we want to always return theRight
object of empty movies which is a success scenario. More important is we return theRight
object, it doesn't matter whether it is an empty list or a filled list. - We will then call the
movieTabChanged
.
expect
expect: [
isA<MovieTabLoading>(),
isA<MovieTabChanged>(),
]
We expect an array of events to be emitted, so the first event would be MovieTabLoading
and the second is MovieTabChanged
. This will ensure that we never mess up the sequence of states that are emitted from this bloc. Run the test case, all the test cases should pass.
verify
We haven't verified that there was an API call for GetPlayingNow
. We want to make sure that for tab index 1, the GetPlayingNow
usecase should be called. It can be the case that any other developer changes the order and calls GetPopular
instead of GetPlayingNow
. To verify write the below code.
verify: (MovieTabbedCubit cubit) {
verify(getPlayingNowMock.call(any)).called(1);
})
You will verify the GetPlayingNow
mock with any
here is called one time. Run the test case, all the test cases should pass. Similarly, you can write test cases for other tabs as well.
The Fail Test
What about the failure scenario. If there is an error from one of the calls, then we emit MovieTabError
.
blocTest(
'should emit [MovieTabLoading, MovieTabLoadError] state when coming soon tab changed fail',
build: () => movieTabbedCubit,
act: (MovieTabbedCubit cubit) {
//1
when(getComingSoonMock.call(NoParams()))
.thenAnswer((_) async => Left(AppError(AppErrorType.api)));
cubit.movieTabChanged(currentTabIndex: 2);
},
expect: [
//2
isA<MovieTabLoading>(),
isA<MovieTabLoadError>(),
],
verify: (MovieTabbedCubit cubit) {
//3
verify(getComingSoonMock.call(any)).called(1);
},
)
- When the
getComingSoon
usecase is called, return theLeft
object with some error type - Expect the error state as the last state in the order.
- Verify that the
getComingSoon
usecase is called.
Movie Detail Bloc Test
Let's consider one more example before saying the end to this tutorial.
blocTest('should load movie success',
build: () => movieDetailCubit,
act: (bloc) async {
//1
when(getMovieDetailMock.call(MovieParams(1)))
.thenAnswer((_) async => Right(MovieDetailEntity()));
//2
bloc.loadMovieDetail(1);
},
//3
expect: [isA<MovieDetailLoaded>()],
verify: (bloc) {
//4
verify(loadingCubitMock.show()).called(1);
verify(castCubitMock.loadCast(1)).called(1);
verify(videosCubitMock.loadVideos(1)).called(1);
verify(favoriteCubitMock.checkIfMovieFavorite(1)).called(1);
verify(loadingCubitMock.hide()).called(1);
},
);
- When the
getMovieDetail
usecase is called, then return theRight
object. - Call the
loadMovieDetail
method with 1 asmovieId
. - Expect that the state is
MovieDetailLoaded
. - Verify that loader was called to show and hide once and other usecase calls were made or not.
This is it from this tutorial, let's write unit test cases and make our code less prone to silly mistakes. Next, we will see UI testing. Put your questions in the comment section. Thanks for reading.