Testing BLoC and Repository in Flutter
Integration and Unit testing in Flutter
Make your app’s features bullet-proof by testing them
Hi Folks! This is the last article for my series “Architect your Flutter project using BLoC pattern”. In this article, you will be learning another most important and critical part of software development i.e “Testing”. You will learn both Integration and Unit testing. This will help you deliver software that meets expectations and prevents unexpected results. So without further delay let’s write some test cases. 😄
Content
- Creating the test directory
- Unit testing the
Network Layer
💻 - Integration testing the
BLoC
- Running the test cases 🏃
Prerequisite
As this article is part of a series. I expect you to go through the previous parts of the series(links below). This will help you understand the whole architecture of MyMovies app and what were changes made in different parts of the series.
Other parts of the series
Part 1
Part 2
Part 3
Goal
Have you ever launched an app without testing? I hope your answer is “NO”. You don’t want your customers to have unexpected crashes or a buggy app. There is a beautiful quote regarding testing “If you don’t like testing your product, most likely your customers won’t like to test it either”.
Software testing is the most crucial part of any software development lifecycle. This will help you find bugs and errors that were made during the development phase. Ultimately this methodology will help you ship quality products to your customers. Today you will be testing the features of MyMovies app which was built in the previous parts of the series.
Unit and Integration test
A unit test is a test written by the programmer to verify that a relatively small piece of code is doing what it is intended to do. They are narrow in scope, they should be easy to write and execute.
Part of being a unit test is the implication that things outside the code under test are mocked or stubbed out. Unit tests shouldn’t have dependencies on outside systems.
An integration test is done to demonstrate that different pieces of the system work together. Integration tests can cover whole applications, and they require much more effort to put together. They usually require resources like database instances and hardware to be allocated for them. The integration tests do a more convincing job of demonstrating the system works (especially to non-programmers) than a set of unit tests can, at least to the extent the integration test environment resembles production.
Note: source StackOverflow
The above explanation is more than enough to understand the difference between unit and integration tests. This will help you understand which part of the code can be unit tested or you need to write integration tests.
Creating the “test” directory
Download the project on your machine from here and open it in your favorite IDE. I will be using Android Studio for the demo. Make sure you download the 2nd branch as it has the Dependency Injection module which will make testing much easier.
Once you open the project in your IDE. The project structure should look as below:
Refactoring dependencies
Before moving to the testing part let’s refactor the approach we followed in the previous article to add inject.dart in the project.
Delete the injection
sub-module from your project. Open your pubspec.yaml
file and delete the following dependencies:
Now update your pubspec.yaml file by adding the following dependencies:
In the above list of dependencies, you have replaced the inject.dart
framework’s path with url
. Now instead of maintaining a local sub-module of the inject.dart
framework you will add it to your project during compile time.
You have also added mockito
which will help you mock some objects while testing. Now run flutter pub get
and everything should work fine.
Creating the test module
Now create a test
directory in your project. This will hold all your test cases. This is how the project structure should look like:
Layers to test
Now let’s discuss what are the layers you are going to test. Below diagram should make things clear:
In the above diagram, you are going to unit test the network layer(MovieApiProvider
). You will check if the logic of parsing the JSON is correct or not and also test how your code behaves when different HTTP status codes are provided.
Next, you will write some integration tests to see if the BLoC, Repository and Network layer modules are integrated logically or not.
Now create two sub-directories inside the test
directory. Name them as unit_test
and integration_test
. These directories will hold a specific type of test cases i.e unit and integration. Now your project structure should look like this:
Unit testing the Network Layer
It’s time to write some unit test cases for the Network layer i.e MovieApiProvider
. Let’s have a look at the current code of the MovieApiProvider
:
In the above code, you will be unit testing the fetchMovieList()
and fetchTrailer()
methods. But you need to refactor the above code so that instead of throwing Exceptions
you will provide a particular state
. So let’s add the necessary code for this.
Create a file state.dart
inside the models
directory and paste below code in it:
Whenever you make a network call, you will either get a success
or error
. So keeping that in mind you will use this class to provide a particular state from your network layer to the upper layers i.e Repository
andBLoC
. This approach will also help you write test cases which will expect a particular state.
Let’s refactor the MovieApiProvider
class as below:
Now instead of directly throwing Exceptions
you are providing a particular state either success
or error
. Now you also need to refactor these classes i.e repository.dart
, movies_bloc.dart
and movie_detail_bloc.dart
.
Open therepository.dart
file and paste below code in it:
You just added the generic State
as the return type. It will hold either asuccess
or error
state.
Open themovie_bloc.dart
file and paste below code in it:
Inside fetchAllMovies()
you are checking the state
and passing the correct data to the stream.
Open the movie_detail_bloc.dart
file and paste below code in it:
Same stuff insidefetchTrailersById()
. Checking the state
and passing the appropriate data to the stream.
The last file you will refactor is the movie_detail.dart
. Copy and paste below code in it:
Previously you were using FutureBuilder
to load the trailer from the network. But now you don’t have to do it anymore as you have fetched the trailer JSON data from the network inside the MovieDetailBloc
class, converted it to TrailerModel
object and added it to the stream. So now you are directly observing the stream which consists of an TrailerModel
object.
Writing unit test cases
Phew! Lot’s of refactoring 😅 but this will ultimately help you test your code with more clarity. Now without any further delay let’s focus on writing some unit test cases.
Since you are using theinject.dart
framework to provide the dependencies to the required classes. You have to create a separate dependency graph for unit tests because they shouldn’t have dependencies on outside systems.
Create the below files inside your unit_test
directory:
Open mock_client.dart
file and paste below code in it:
You have created a MockClient
which will provide you with different Http response in each test. This will not make a real network request. 😃 The purpose of a unit test is to test the logic of the code and not the final functional requirement.
Now open provider_injector.dart
file and paste below code in it:
You will see an error at line number 2. This file will be generated by build_runner
during compile time.
Open provider_module.dart
file and paste below code in it:
Here if you take a closer look at line number 12. You are injecting a MockClient
object instead of http.Client
object. We are just providing mock objects because the purpose of a unit test here is just to test the piece of code which will handle the network response.
Now is the time where you will write your first unit test case. Open the provider_test.dart
file and paste below code in it:
Let’s go through the above code step by step:
- You need to have a
main()
method where you will put all your test cases. group()
is a method which will hold multiple test cases.test()
is a method where you will write your single test case. If you want to create multiple test cases you have to declare multipletest()
methods and write individual test cases in it.expect()
will take two parameters and check if both are same or not. This is where your test case will either pass or fail. In the above code, the first parameter in theexpect()
is the actual type and the second is the matcher type.- You are passing different JSON body and status code to
fetchMovieList()
andfetchTrailer()
to check if these methods return the expected state.
Running your first unit test
Open your terminal and navigate to the root of your project. Run the below command:
flutter packages pub run build_runner build --delete-conflicting-outputs
If you got the Succeeded
message then everything is working fine 👏. Now a file named provider_injector_inject.dart
is added in your unit_test
directory. If you open the file you will see all the required objects are created and are ready to be injected. Now the error in your provider_test.dart
file should have gone.
Go back to provider_test.dart
file and click on the green button beside the main()
method. You will see the following output:
The test cases are failing because you forgot to add the API key in your MovieApiProvider
class. If you have already added the key then you will see the following output:
Yay!! 👏 👏 You have successfully run all your unit test cases. You have tested both fetchMovieList()
and fetchTrailer()
method’s logic with different scenarios.
Integration testing the BLoC
Now let’s write some integration test cases and see how the individual modules integrate together.
Create a file bloc_test.dart
under theintegration_test
directory and paste below code in it:
You must be wondering why we didn’t create a separate dependency graph for this. The answer is very simple, you don’t have to provide any mock objects. We will be using the project’s DI
module to provide the dependencies because you are actually making a network call and checking the complete integration between network layer, repository and bloc modules.
Running the test cases
Now press the green button beside themain()
method and you should see the following output:
This success result shows that the network, repository and bloc modules are integrated correctly and are behaving as expected.
We have reached the end of this article. I hope you have learnt how to unit test your code and write some integration tests. Next time whenever you make a change to your codebase, just run these test cases to make sure that everything is working fine. If you facing any issues put a comment or connect with me at LinkedIn or follow me on Twitter.
📝 Read this story later in Journal.
👩💻 Wake up every Sunday morning to the week’s most noteworthy stories in Tech waiting in your inbox. Read the Noteworthy in Tech newsletter.