19. Bloc Testing

19. Bloc Testing

ยท

7 min read

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,
  );
});
  1. Initialize the mocks. GetPopularMock, GetPlayingNowMock and GetComingSoonMock.
  2. 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.

  1. Emit the MovieTabLoading state.
  2. The movie is returned as a success.
  3. 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);
}
  1. When getPlayingMock is executed in the cubit, we want to always return the Right object of empty movies which is a success scenario. More important is we return the Right object, it doesn't matter whether it is an empty list or a filled list.
  2. 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);
  },
)
  1. When the getComingSoon usecase is called, return the Left object with some error type
  2. Expect the error state as the last state in the order.
  3. 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);
  },
);
  1. When the getMovieDetail usecase is called, then return the Right object.
  2. Call the loadMovieDetail method with 1 as movieId.
  3. Expect that the state is MovieDetailLoaded.
  4. 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.

Did you find this article valuable?

Support Prateek Sharma by becoming a sponsor. Any amount is appreciated!

ย