Unit testing in Flutter: http requests
With this article I start a new series about testing with Flutter, where I’ll be presenting how to test the main actors in a typical app: models, providers, blocs and widgets.
In this first article I want to demonstrate how to unit test a provider used in a Flutter application to create users in a backend service via http.
Let’s imagine we have a Flutter application with a sign in screen where the user introduces a username and password and presses a button to register. We have also a custom backend that deals with the user creation and authentication tokens.
The following diagram illustrates the sequence of events happening and our different use cases.
The catch here is that I will always try to do a post call first. If I get a 409 status code in the response it means the user already exists and so I will do a patch instead (to update a timestamp or something similar).
What code do I want to test?
The code that I use here has been simplified a lot so that I can abstract from complex authentication flows but I can still use it to showcase some testing scenarios.
I’ll be using a class UserResponse which represents the user information returned from the backend.
The class CertHttpProvider is used to deal with certifications, and will be used as an injected dependency in IdentityProvider, which will be our Subject Under Test. As we should always depend on abstractions and not on implementations, I have also defined an abstract HttpProvider, implemented by CertHttpProvider, but that will be the one used by our sut.
Finally, IdentityProvider exposes only a create() method which eventually will return a User or will throw an exception.
The create() method does as we designed in our previous diagram. If the POST call returns a 409 a PATCH will be sent. If there is any other error, the execution will be aborted and an exception thrown.
Again, remember that the code has been simplified. In real life we would use different models for the response and the identity and a more complex and robust authentication flow.
We are going to focus the tests in our IdentityProvider. Testing our models it’s fairly simple but the provider is where our core logic resides and we want to make sure we have every case covered. The three different scenarios to test are:
- when the user does not exist, create() returns a User.
- when the user exists, create() returns a User.
- when there is an unexpected error from the backend, create() throws an exception.
Mocking and faking
As we want to unit test our IdentityProvider we need to mock our backend and use some fake data as responses.
As my IdentityProvider gets the HttpProvider as an injected dependency it’s very easy to use a mock HttpProvider in my tests, because I don’t want to do real requests here. I could manually mock my HttpProvider but I’m going to use mockito instead; I just need to create a new class that will extend Mock (provided by mockito) and implements the class that I want to mock, and mockito will generate a mock for each method in the class.
FakeUserResponse is the data that I’m going to use as response for a successful call to our mocked backend.
The variable expected will be used in our tests to verify that the result of our calls is what we really expect. We won’t use this in every test, but as this will be returned in two of our tests we can define it as final and share it by both of them.
Finally, setUp() and tearDown() are used to prepare and clear the environment before each test in our suite. Basically, what we are doing in this case is initializing our sut and closing the client connection.
Thanks to mockito I have mocked implementations of post() and path(), which I can now use to spy my calls and return whatever I need in each test. Let’s define two helper functions that will be very handy in all our tests:
For stubPost(), we want that whenever a POST call is made to a url starting with the param provided, we will get the response also passed as a parameter. For stubPatch() it’s exactly the same.
Test user creation when the user does not exist
Easy one, when the user does not exist, our POST will return a 201, and our second call to retrieve the token will also return a 201.
Test user creation when the user already exists
In this case, the first POST will return a 409, and the PATCH a 201 with the proper user. Finally, the POST to retrieve the token will return a 201.
We then execute our signIn() method, calls will be intercepted and we will get our user with the successful PATCH call.
Test user creation when there is an unexpected error
Also an easy test, we just need to return a 500 status code. This will not trigger a PATCH but will just throw an Exception instead.
That’s the complete example of my test class, including the imports that I need:
That’s all, you can see how with some minimal boilerplate we can write very lean tests to verify our test cases. Again, remember I simplified quite a bit the code to try to focus on the tests, so please feel free to write in the comments if you want some further explanations.
Thanks for reading!