Top 4 Test Types for Flutter Production Teams
Improve your quality and speed of releases with these types of testing.
Most of the developers and teams I talk to use the slowest, and most expensive way of checking their app’s quality. But this is just 1 out of the 4 ways production teams ensure their application is of high quality.
We know that testing is important to ensure quality applications, but knowing which tests to invest in makes sure your time is well spent.
In this article I’ll share with you:
A framework to easily identify types of tests
An explanation of each test type and when to use it
How to automate tests without additional dev effort
How to identify types of tests
You’ve probably heard of many different kinds of tests. We’ve found that only 4 have proven to have a direct and sometimes, immediate impact on our quality and speed of delivery. Only 3 of them are automated.
There is Unit testing, Integration testing, End-to-End testing, and Manual testing.
My easy system to remember the definitions of these tests goes as follows.
Imagine your application divided into 5 lanes, User, UI, State, Business Logic, and Backend (see image below)
A test that only operates in a single lane is a unit test
A test that covers 2 or more lanes is an integration test
A test that covers all the lanes is an end-to-end test
It’s that simple. No fancy explanations or technical terms are required.
Now let's dive into the first form of automated testing, unit tests.
Unit Testing
This is a form of testing that allows you to test your code at the smallest level. All software is made up of small components that together form a larger feature. There’s code you can write to individually test those components (units). This gives you unit tests.
Unit tests are there to ensure that the individual behavior of a single object matches what you expect given a specific input. It’s not about “The order cart feature” it’s more about “Checking if there are no duplicate items in the cart” or “Ensuring that when adding an item that exists it increments the item quantity”.
Tests to write
The most valuable types of unit tests to write with unit tests are:
Utility functions: This is usually done by formatting or converting data. Data goes in, and different data comes out as a result. Each rule of the function should be a unit test.
State changes: When this function is called, the state of the object should now be X.
Interactions between objects: When this function is called, we expect to pass X to function Y on service Z. This serves as a guarantee that the same business logic is still in place for a function.
Error handling: When calling this function in state X, or with parameter Y, it should throw this exception. This ensures that you understand which states/inputs are required to trigger exceptions or errors in your code.
How your code responds to results: When calling this function and the result from calling ServiceX is null, does my code retry the logic? These types of tests allow you to have a robust set of circumstances that prove that the code you wrote will work regardless of the result from the objects your function depends on.
Testing Conditional execution paths: When given X, Y should be executed. When given Y, X should be executed. This serves as a way to ensure that current execution paths are the same as, inevitably, more will be introduced and the old ones will be altered.
Testing Assumptions / Expectations: At certain points in code we expect values to, not be null, states to be in a specific point, or the code not to throw an exception. Adding tests that confirm those assumptions are true, gives you hard proof that those assumptions are still true. These are arguably the tests that prevent the most bugs in production because it allows the developer that originally writes the test to provide concrete evidence of the assumptions they expect to be true for their code to work.
Integration Testing
This form of testing allows you to test interactions between your backend or device and your code. These tests use the real functionality with pre-determined data, to ensure that your system is interacting the way you expect it to with everything outside of your system.
Unit tests can cover deterministic behaviors of your system based on pre-defined input and pre-defined responses. Integration tests help you go outside of your system with pre-defined input and real responses. Testing the saving of data to the database. Making a real HTTP request and seeing if your app still responds the same.
Integration tests have proven to be the most valuable when you need to ensure that the backend your code is interacting with is still compatible with the version of code that you’re running. If the backend changes outside of your control then this is an important set of tests to have.
Test Goals:
Model Synchronization: When you interact with a backend it’s common for the backend and the front-end to have a representation of the same model. This often can get out of sync if the backend developer is separate from the front-end developer. By writing an integration test that executes a code path where data is fetched and serialized it gives you a way to quickly check if everything is still working when a new backend update is pushed.
Backend mutations: Some functionality requires us to send data to the backend where the backend will use that and push it through our system. This often results in additional state changes that we assume to have happened. For instance, creating an account for a user and expecting the backend to create their customer Id for stripe payments. Integration tests allow you to write a simple test that creates a real user, then gets the user from the backend and checks if all the properties have been set.
End-to-End Testing
This form of testing takes your application as it is and uses it “as if it’s a user”. The application is started and we perform touches on the screen and inspect visually if the application’s UI is in the state we expect it to be.
This is commonly added to reduce the time manual testers require to go through the entire app and manually check all the functionality. It takes the longest to run and also comes with the highest maintenance requirements. It takes a large amount of effort to write the tests and an additional amount of effort to maintain them as the application grows.
Test Goals:
User journey testing: This type of testing allows you to confirm if a single user journey works end-to-end. The best use case of this is to take the most crucial test cases that are performed manually and automate those first.
Manual Testing
This is the most common form of testing in production teams. A person launches your app and uses ALL the features in your app to confirm everything still works as expected. As you can imagine this takes the longest out of all forms of testing.
The goal of this process is to ensure that the application works as the user expects it to work. Also known as user acceptance testing. In addition to that, manual testing is a great tool to find edge cases where your application breaks or produces unexpected behaviors.
The type of tests you perform in this phase is done per feature and per feature functionality. You use the feature, in all the ways you can come up with and you repeat that for every feature and every functionality within a feature.
Wrap up
This is not an all-or-nothing situation. Most teams have manual testing, adding only one form of these will give you significant boosts in your confidence in the code and your speed of making releases. Take the leap, and start adding some automated tests in your code. Reach out of you or your team needs help to do that.
If you enjoyed this, subscribe to get more like this directly in your inbox every week.
Let’s work together
Book a 1-on-1 call with me to take the next steps for your Flutter project.