Unit Testing With Flutter: Getting Started
In this Unit Testing with Flutter tutorial, you’ll improve your programming skills by learning how to add unit tests to your apps. By Lawrence Tan 🇸🇬.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Unit Testing With Flutter: Getting Started
20 mins
- What is Unit Testing?
- Testable Architecture
- Getting Started
- Explore the Begin Project
- Project Structure
- Dependency Injection With GetIt
- Project Architecture Summary
- Product List
- Cart
- Setting up Firebase
- Creating a Firebase Project
- Importing JSON Files
- Unit Testing the Product List Feature
- Mocking
- 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.
Testable Architecture
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!
Getting Started
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.
Project Structure
The 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:
- A
BaseModel
class that inherits fromChangeNotifier
, which uses theObserver
pattern to notify listeners of any changes in the model. CallingnotifyListeners()
rebuilds the widget tree, allowing the UI to react as the model is updated. -
CartModel
extends and inheritsBaseModel
so it can be easily observed by the UI. You’ll flesh out this class later on. - Finally,
ProductListModel
is 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 ChangeNotifier.notifyListeners
.
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.
Project Architecture Summary
Here’s a quick recap of how the app is architected.
The product list feature is split into a Model, View, and ViewModel as follows:
Product List
- Model: Products
- View: ProductList View
- ViewModel ProductList Model
And the cart feature is split like so:
Cart
- Model: Products
- View: Cart View
- ViewModel Cart Model
All unit tests will be performed on the ViewModel. Being able to test the ViewModel is one of the main benefits of using the MVVM architecture.
Now that you’ve explored the starter package, you can setup your own instance of Firebase and then get testing!
Setting up Firebase
In order to have products for your users to buy, you need to use a database for ShopNBuy. In this case, you’ll use the Firebase Realtime Database, which allows you to seamlessly upload data using JSON files.
If you’re already familiar with setting up a Firebase Realtime Database, you can jump right into the next section, Importing JSON Files.
Creating a Firebase Project
Head to the Firebase Console and log in with your Gmail account. Click Add Project and name the project ShopNBuy, then click Continue.
Disable Google Analytics; you won’t need them for this tutorial. Click Create Project, wait for the setup to finish, and continue to the Firebase Dashboard.
From the left menu, click Database.
Next, scroll down and click on Create Database under Realtime Database. Then check Test Mode and click Enable.
Importing JSON Files
Now that you have your database set up, it’s time to import some products.
In the Data section, click on the Menu icon, which looks like three vertical dots, then choose Import JSON. Then click Browse, navigate to the Download Materials root folder and look for products_list.json. Finally, click Import to upload all the data you need for this tutorial.
Copy the URL from the top of the card. In constants.dart, replace [YOUR OWN FIREBASE URL]
with that Firebase URL. For example, https://shopnbuy-12345.firebaseio.com/products.json.
Build and run the app. You should now see a list of products in your app.
Now that you have Firebase ready, it’s time to start unit testing the app!
Unit Testing the Product List Feature
Your next step is to add tests to make sure that the product list feature works properly. Before you can add the tests, you need to know the feature’s requirements:
Before you start writing the tests, take a moment to learn about an important concept in testing: mocking.
Mocking
Mocking is a way of simulating parts of your app that are outside the scope of the test so that they behave in a specific way. Using mocking in your unit tests prevents tests from failing for reasons other than a flaw in your code, like an unstable network. It allows you to conveniently assert and validate outcomes and gives you the ability to control input and output.
Unit testing means you test individual units of code without external dependencies to keep the integrity of each function or method pure. By mocking your data models and classes, you remove dependencies from your tests.
If you made real network calls in your unit tests, your test suite could fail due to an internet problem. To avoid this, you’ll mock API
so you can dictate its response.
Add this code at TODO 2 in product_list_test.dart
:
import 'package:shopnbuy/core/models/product.dart';
import 'package:shopnbuy/core/services/api.dart';
class MockAPI extends API {
@override
Future<List<Product>> getProducts() {
return Future.value([
Product(id: 1, name: 'MacBook Pro 16-inch model', price: 2399,
imageUrl: 'imageUrl'),
Product(id: 2, name: 'AirPods Pro', price: 249, imageUrl: 'imageUrl'),
]);
}
}
You just created a new MockAPI class that extends the real API. You override getProducts()
and hardcode two products. This makes the function return the hard-coded data instead of downloading live data from Firebase, which would make your test slow and unpredictable.
This is one of the main benefits of mocking: You can override a method to dictate its value as the test suite runs.
In the main()
body, add the following code after TODO 3:
// 1
setupDependencyAssembler();
// 2
final productListViewModel = dependencyAssembler<ProductListModel>();
// 3
productListViewModel.api = MockAPI();
Step-by-step, here’s what’s happening:
- The test suite runs separately from
main()
inmain.dart
, so you need to callsetupDependencyAssembler()
to inject your dependencies. - You create an instance of
ProductListModel
using GetIt. - You create and assign an instance of the
MockAPI
class you defined above.
Make sure to import the product list model and the dependency assembler at the top of the file:
import 'package:shopnbuy/core/viewmodels/product_list_model.dart';
import 'package:shopnbuy/helpers/dependency_assembly.dart';
Now, it’s time to write your first few test cases!
Writing Test Cases
Start by adding this code after TODO 4:
group('Given Product List Page Loads', () {
test('Page should load a list of products from firebase', () async {
// 1
await productListViewModel.getProducts();
// 2
expect(productListViewModel.products.length, 2);
// 3
expect(productListViewModel.products[0].name, 'MacBook Pro 16-inch model');
expect(productListViewModel.products[0].price, 2399);
expect(productListViewModel.products[1].name, 'AirPods Pro');
expect(productListViewModel.products[1].price, 249);
});
});
Welcome to unit testing in Flutter! Flutter uses the common group and test paradigm for writing unit tests. You can think of a group as a way to organize different unit tests around a specific theme. It’s often helpful to think of these testing blocks as sentences – the group
function is an event that happened, and the test function is the result that you want to ensure. You can have as many test
blocks within a group
closure as you want.
Here, you took your requirement statement and broke it into test statements. You’re testing that when a user enters the product page, they see a list of products.
Here are some more details about what the code does:
- Since the function passed to the
test
method is marked as async, each line in the closure runs synchronously, so you start by callinggetProducts()
. - You then assert the length of the list based on the mock data you supplied in the
MockAPI
. - Finally, you assert each product’s name and price.
To have access to the test
and group
methods, you’ll need to import the test package.
import 'package:flutter_test/flutter_test.dart';
With these test cases written, you’re ready to run your tests. If you’re still running your app, stop it using Control-C and run flutter test in the terminal. After a few seconds, you’ll see:
00:02 +1: All tests passed!
Hurray! You’ve successfully written your first Flutter test – and it passed.
If you’re using Visual Studio Code, you can also run your tests by going to Debug ▸ Start Debugging with product_list_test.dart open. In the left navigator panel, you’ll see green checks to the left of each statement signifying that the tests have all passed!
In Android Studio click the green Play button to the left of the test header to run your tests.
Unit Testing the “Add to Cart” Logic
Now that you’ve written the logic, it’s time to write your tests.
Head to product_list_test.dart and replace TODO 5 with this:
final Product mockProduct = Product(id: 1, name: 'Product1', price: 111,
imageUrl: 'imageUrl');
Here, you declare a mock product so you can simulate adding a product to the cart.
Next, replace TODO 6 with:
final cartViewModel = dependencyAssembler<CartModel>();
and add the required import:
import 'package:shopnbuy/core/viewmodels/cart_model.dart';
This will create an instance of the cart view model in the test suite.
Finally, replace TODO 7 with the following:
test('when user adds a product to cart, badge counter should increment by 1', () {
cartViewModel.addToCart(mockProduct);
expect((cartViewModel.cartSize), 1);
});
Here, you assert that adding a product to the cart will increment the cart size. Run Flutter test in the terminal, or if you are using Visual Studio Code, press Run ▸ Debug
just above the start of the test closure to run the test.
You’ll see this:
00:02 +2: All tests passed!
Congratulations, you’ve written your second test and it passed!
Unit Testing the Shopping Cart Feature
The shopping cart appears to work properly, so it’s time to add your tests.
Head to cart_test.dart and add the following under TODO 8
List<Product> mockProducts = [
Product(id: 1, name: 'Product1', price: 111, imageUrl: 'imageUrl'),
Product(id: 2, name: 'Product2', price: 222, imageUrl: 'imageUrl'),
Product(id: 3, name: 'Product3', price: 333, imageUrl: 'imageUrl'),
Product(id: 4, name: 'Product4', price: 444, imageUrl: 'imageUrl'),
];
And import the Product
class at the top of the file:
import 'package:shopnbuy/core/models/product.dart';
This declares new mock products.
Then, add the following to TODO 9:
// TODO 9: Call Dependency Injector
setupDependencyAssembler();
This calls the dependency injector just like you did in the other test. Again, make sure to import the setupDependencyAssembler
function:
import 'package:shopnbuy/helpers/dependency_assembly.dart';
Next, add the following to TODO 10:
var cartViewModel = dependencyAssembler<CartModel>();
Just like before this will create an instance of your view model.
Again, import the CartModel
class:
import 'package:shopnbuy/core/viewmodels/cart_model.dart';
Finally, add mock products to the cart, replacing TODO 11 with the following:
cartViewModel.addToCart(mockProducts[0]);
cartViewModel.addToCart(mockProducts[1]);
cartViewModel.addToCart(mockProducts[2]);
cartViewModel.addToCart(mockProducts[3]);
cartViewModel.addToCart(mockProducts[0]);
cartViewModel.addToCart(mockProducts[1]);
Testing Your Shopping Cart
Now that you’ve added lots of products to your view model, it’s time to write the tests!
Add this test suite at TODO 12:
group('Given Cart Page Loads', () {
// 1
test('Page should load list of products added to cart', () async {
expect(cartViewModel.cartSize, 6);
expect(cartViewModel.getCartSummary().keys.length, 4);
});
// 2
test(
'Page should consolidate products in cart and show accurate summary data',
() {
cartViewModel.getCartSummary();
expect(cartViewModel.getProduct(0).id, 1);
expect(cartViewModel.getProduct(1).id, 2);
expect(cartViewModel.getProduct(2).id, 3);
expect(cartViewModel.getProduct(3).id, 4);
expect(cartViewModel.getProductQuantity(0), 2);
expect(cartViewModel.getProductQuantity(1), 2);
expect(cartViewModel.getProductQuantity(2), 1);
expect(cartViewModel.getProductQuantity(3), 1);
});
});
Again, make sure to add the following import at the top of the file:
import 'package:flutter_test/flutter_test.dart';
Here you have tests for:
- The assertion of the cart size and the unique products that the cart shows.
- The assertion that the products displayed in each index should match their equivalent data.
Run Flutter test and you’ll see:
00:02 +4: All tests passed!
Four test cases passed. Rejoice!
Challenge: Unit Testing the Checkout Feature
In this final step, you’ll unit test the checkout feature. Try it out on your own or take a peek at the solution first, then attempt the challenge. The best way to learn is to do it yourself!
There are two main requirements for this set of tests:
- Test #1 When the user confirms the purchase, it should show the total cost of all of the items in the cart.
- Test #2 When the user has finished the purchase, it should clear the cart.
If you got stuck or want to compare your answer with the final solution, click Reveal to open the solution section.
[spoiler]
Head to cart_test.dart and add these test cases after the previous two test cases within the group:
// 1
test('When user confirms the purchase, it should show total costs', () {
expect(cartViewModel.totalCost, 1443);
});
// 2
test('When user has finished the purchase, it should clear the cart', () {
cartViewModel.clearCart();
expect(cartViewModel.cartSize, 0);
});
This checks:
- The assertion of the total costs, calculated by totaling all the selected products.
- The assertion that the cart clears after checkout.
Run Flutter test and you should see:
00:02 +6: All tests passed!
Woohoo! All six test cases have passed and you deserve three stars! You’re now ready to start writing unit tests in your Flutter apps.
[/spoiler]
Where to Go From Here?
You can download the final project by clicking the Download Materials button at the top or bottom of this tutorial.
For your next steps, expand your Flutter testing knowledge by exploring the official UI tests cookbook from the Flutter team. You can also take your testing to the next level by exploring and applying Test-Driven Development.
We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more