Unit Testing With Flutter: Getting Started
- What is Unit Testing?
- Testable Architecture
- Getting Started
- Explore the Begin Project
- Project Structure
- Dependency Injection With GetIt
- Project Architecture Summary
- Product List
- Setting up Firebase
- Creating a Firebase Project
- Importing JSON Files
- Unit Testing the Product List Feature
- Writing Test Cases
- Unit Testing the “Add to Cart” Logic
- Unit Testing the Shopping Cart Feature
- Testing Your Shopping Cart
- Challenge: Unit Testing the Checkout Feature
- Where to Go From Here?
So you’re a Flutter developer who wants to improve your skills and write better apps. Great! One way to be a better developer is to apply Unit Testing to your Flutter apps.
Writing unit tests for your Flutter app can help you create resilient and bug-free code.
In this tutorial, you’ll learn how to write unit tests for your flutter app by building and testing a simple shopping cart app. In the process you’ll learn:
- Testable Architecture (MVVM)
- Mocking in Dart
- How to use GetIt for dependency injection
- How to write unit tests for a Flutter app
What is Unit Testing?
Unit testing is a process where you check quality, performance or reliability by writing extra testing code that ensures your app logic works the way you expect before putting it into widespread use. Picture walking into your favorite apparel shop, taking a trendy shirt and going into the fitting room.
In that fitting room, you’re performing a unit test. You’re ensuring that your hands can slot through the shirt’s sleeves, the buttons are in place and the style and size match your figure.
Writing unit tests helps you to confidently build new features, minimize bugs and painlessly refactor existing code. It also forces you to write better, more maintainable code that can be easily tested.
Unit testing focuses on testing the logic of your app rather than the actual user interface.
Before you can start writing unit tests, you need to structure your code in such a way that you can easily test a single component of your app.
Traditional patterns like Model-View-Controller (MVC) force you to perform testing on the view controller, which is often tightly coupled with the view. That means that instead of testing a single piece of logic, you’re instead testing the entire view in addition to that piece of logic.
One rule of thumb when building a testable architecture is that you should never import any UI-related packages into your unit test suite. If you are importing UI-related packages, it means your unit tests are testing Flutter widgets instead of app logic, which isn’t what you want.
The Model-View-ViewModel (MVVM) architecture separates business and presentational logic to make writing unit tests much easier. The MVVM pattern allows you to transform data models into another representation for a view.
For example, in the app you’ll test in this tutorial, you use a view model to transform JSON data about server-side products into a list of objects. This allows the list view to display a list of products without having to be concerned about how the data got there.
All the processing logic resides in view models, which are isolated from the views. Isolating processing logic from views allows you to focus your testing efforts on your implementation logic.
With these concepts equipped, you’re ready to dive into the starter project to see it in action!
To start, download the begin project by clicking the Download Materials button at the top or bottom of the tutorial, then explore the starter project in Visual Studio Code. You can also use Android Studio, but this tutorial uses Visual Studio Code in its examples.
For this tutorial, you’ll write unit tests for an e-commerce app called ShopNBuy.
ShopNBuy allows users to:
- View a list of products from Firebase.
- Add product(s) to a shopping cart.
- Perform a cart checkout.
That said, before you start writing tests, take a look around the project.
Explore the Begin Project
The begin package includes the entire implementation of the app so that you can focus on testing. Take a quick look at the contents of lib to understand what’s there.
Product class, under models, is a simple model object which represents a shopping list product item.
In services, you’ll find an
API class that handles making a network call to fetch a list of products.
Under viewmodels, you’ll find three important classes:
BaseModelclass that inherits from
ChangeNotifier, which uses the
Observerpattern to notify listeners of any changes in the model. Calling
notifyListeners()rebuilds the widget tree, allowing the UI to react as the model is updated.
CartModelextends and inherits
BaseModelso it can be easily observed by the UI. You’ll flesh out this class later on.
ProductListModelis a container for a list of products.
In helpers, you’ll find important constants in constants.dart. You’ll also see dependency_assembly.dart, where the dependencies are setup using GetIt, a dependency injection package.
The last important piece of code is the
BaseView class under ui/views.
BaseView is a wrapper widget which contains the
provider state management logic. It also contains a
model property that you’ll inject as a dependency.
The base widget is wrapped in a
ChangeNotifierProvider, which listens to a
ChangeNotifier, exposes it to its descendants and rebuilds dependencies whenever you call
Finally, switch to widgets, where you’ll find a few UI-related widgets that build the Cart Screen and the Product List Screen. Read through them now to understand how they are implemented.
Dependency Injection With GetIt
When writing testable code you’ll find that you need to supply dependencies to the classes that you want to test. That way when you’re writing a unit test you can test the relevant component in isolation. For example, if you’re trying to test an object that makes a network call and then parses the response, you’ll want to make sure that the object isn’t actually making a call over the network, since that would be very slow and brittle. Instead, you’ll often want to inject another object that actually makes the network call into the component so you can provide a fake version in your unit test.
GetIt allows you to register a class and request it from anywhere. Most notable for this app, you can register a view model as a
Factory, which returns a new instance every time a view opens.
You can register classes like
API which you’ll normally only need one of as a
LazySingleton, which will always return the same instance.