Let's talk about Unit Test (Flutter)
Are you ready??
What is Unit Test and what's it importance?
Well, we have a common practice among developers, to write tests or Unit Tests, more specifically, to the functions your are creating. But, why is that so important?
Let's think that we have this dart example code. What would you do if you wanted to know if your could would run as expected before running it?
That's where unit tests comes in!
You can write some tests and asserts to check the expected responses for your code, for example, we can write a simple unit test and see if Awesome is returning the expected result.
Ok, ok, that test was too simple I know, but it was just to let you see how things can be done.
Imagine now that you have a much bigger and complex function and you want to refactor it! How many hours will you loose testing every change you made to see if it had affected any other part of your code?
This is THE THING about unit tests, once you have to change or refactor any part of your code, you can simply run the tests and find if you made any mistake. Listen, Unit tests are one of the quicker ways to find any error in your project, you can trust me!
What's is so special about that?
Well, you are not convinced yet? So, lets think a little bit more. Imagine now that you want tu build a CRUD for your app. You will have to handle the sign in, register, update, session management, password recover... Oh man, I'm getting tired of just thinking!
After all of that, you will need to implement your Ui for the app, the inputs and everything else. Once you finished it time to see if things are working...
You can see how many hours did you lost here?
Once you start with tests you will learn about methodologies to split your code, use some abstractions that will let for example, change your entire database without breaking a single feature in your app.
But of course, not everything is unit test!
You can combine your unit tests with Widget and Integration tests as well, that will make your code much more trustable.
Integration x Unit tests
Integration are a little bit different from Unit, it will provide you the ability to check how your functions connect with each other, once the unit tests are "black boxes" (that means, it only knows the context in what it is written, and don't know anything else about the project or implementation), the Integration are "white box".
Those tests are more detailed, covering more pieces of the code and also safer. So, why not use only integration instead of unit?
Well, unit tests are much faster to write and run, once you don't need to know the rest of the application to create the test for your feature, you can mock it and create a "fake execution" of those functions. Therefore that don't need to execute everything and also async functions, that will give a huge save in the execution time.
As a consequence of being faster and easier to create, they are cheaper!
And again, once they don't need to know the entire project, they can catch erros much faster.
Firebase and unit tests
I was trying to find some examples of unit test with firebase but I din't find anything that got my attention, so I decide to create my architecture and present to you!
I used the Reso Coder architecture as a base of this one, if you want to know more about it, go to his page, all of his tutorials are there for free!
The idea here is to separate your Ui, Domain and Data.
Yeah, you will need to write more code! Yeah, you will have many folders in your project! But once you your this kind of structure you will be able for example, to create all the functionalities of your application before touching in one widget, or just change the entire UI without change anything about the business.
Let's think with me for a second.
We have our Movie App, the user can sign in, the movies are displaying nicely from the api, but at some time, we decided to change the database, and now we are starting to use Firebase.
How much change will we need?
Once you have all your layers separated and well defined, you can do this really quick. And the tests will help you to guarantee that everything is ok.
So let's stop talking and show some code!
Starting with the tests
We will need some packages for that, like `firebase_auth`, `firebase_core`, `mockito` and `meta`.
Let's imagine one of our use cases, the authentication. So, of course, let's create a file to handle it. We will create a folder `domain`, and a folder `usecases` inside of it. And then we will have our first use case `sign_in.dart`.
So simple huh? In fact it is a simple function that for know, does nothing.
We will need to have a repository to work on that use case right? The idea of the repository is to handle all of the expected actions for the user, in this case, the authentication.
I will create the file under `domain/repositories`. First let's create an abstraction for our repository, it will help us to maintain the app later and it's also easier to read.
Now that we have our repository abstraction, let's create the implementation for it.
Once the repository implementation will handle the data manipulation and storage, lets save it on the `data/repository` folder.
Once again, this function does nothing, so let's starting increasing the complexity.
We will change the return for our functions a little bit, in order we will need to have some erros handlers, so lets return a type that will provide us the ability to return a failure or a data, for that we will use `Either` type (Thanks again Reso).
The Either will have a left and a right side, we will use the left for failures and right for success. BTW, I talked about the use of abstraction, so let's do the same on our usecases and create a default UseCases class. You can place it on `core/usecases`.
And also lets start implementing something to our usecase, and now it will look like:
As you can see, now we are calling our repository inside our use case. It is a very good abstraction to implement and make your code cleaner. The Either type is now there, in case of error, it will return a failure otherwise it will return our UserEntity.
We can do the same to our repository and starting creating functionalities for it.
Now we are accessing the firebase, getting the user and using a remoteDataSource to fetch the data from our user. Once the focus here is just the unit tests, I will go a little faster with that, you will be able to see the gists on the bottom of the article.
Now let's start with our tests...
First of all, you need to know about Mockito. It's a dart package that allow you to mock your data into your tests.
For example, let's create a test for our signIn usecase.
Like I said, some functions must be mocked, we will do this to our Repository.
Now we are able to use it in our test.
I told you that things was going to get more complex.
In this test file we are creating a mock for our repository to simulate a call in our function. We can use `when` from Mockito, so set expected results.
So, basically, when the repository call the function to authenticate, that is inside the signIn usecase, it will return a Success (Remember? right side of the either) with our UserEntity with the values defined as tUserModel.
Then we will test if the result is the one we expected and check if the `authenticate` from the repository was called.
Let's go back to our repository now. We will add some erros that could be throw but the function for example, in case of incorrect password or some kind of error when trying to save on Firestone.
I'm simulating here some errors that the firebase and Firestone should return, of course the exceptions are a little bit different, you will need to think and create your own handler.
Backing to the test of the repository, we will first create a mock for all functions and responses that we will need.
We will also create instances for each one and set everything up.
As you can see, in the SignIn usecase we mocked the UserRepository, but now we want to check it, and see if everything is going fine, so we will mock only the functions that it will call and simulate all the returns, but we will use a real instance of the UserRepository for that.
Everything we need to do now is call the mocked functions and check the result. But we have a little trick in there. once the `mockAuthResult` have an `user` prop with null value, we will simulate that the `mockAuthResult.user` is returning a FirebaseUser, that is also mocked, and so we will be able to access the `uid` and validate that everything went fine.
Now if we want, we can create our UI with that function that we at least know that will not crash our app or result in error, but to cover the total funcionaliza we should create also the integration tests, but that is a subject for another article.
Hope you all enjoyed.