Image by pch.vector on Freepik, edited with GoDaddy Studio Flutter Test Guide Preface Dear reader and Flutter enthusiast, Welcome to this guide about testing with a focus on Flutter. From my experience, testing is an underrated and undervalued task. Many people don’t see benefits in testing and many programmers prefer developing features instead of writing tests. That’s why I decided to publish a compact guide about testing with Flutter. After studying this ebook, you’ll have theoretical knowledge about testing and lots of working code examples so that you can write better tests in the future or start writing tests at all. This ebook is for you if you… want to have an all-in-one guide about testing Flutter applications have started developing Flutter apps just recently have no or limited theoretical knowledge about testing want to start writing good unit and widget tests for your Flutter apps want to improve your testing skills This guide contains many code examples which can also be found on GitHub. They are free and open-source, so don't hesitate to study and use them if you like. The covered contents include the following: ▶ Testing in software projects General knowledge about testing, naming schemes, code coverage, test types, best practices ▶ Unit tests Hands-on example how to write unit tests in a Flutter application with source code ▶ Widget tests Hands-on example how to write widget tests in a Flutter application with source code ▶ Dependency mocking Technique to reduce testing down to the parts which you really want to test with source code ▶ Measuring code coverage Finding untested code parts by measuring code coverage ➤ Testing is no rocket science! Everybody can do it with a little help. And this ebook might be all the help you need to get started! 2 Flutter Test Guide About the author I have been a professional software developer for over 10 years and I started coding at the age of 15. Since then, I worked for many companies, used many different technologies and programming languages, and gathered a lot of experience in developing software for various industries. My first Flutter project was in 2019 and as the Flutter framework matured, my knowledge increased as well. Today, Flutter is my first choice when it comes to developing crossplatform applications. I also am a content creator on Medium where I publish lots of coding tutorials from the Flutter world. All my code produced for any articles is available on GitHub. You can find more information about me with the following links My Medium profile My GitHub profile Some freebies, curated lists, and contact information 2 3 Flutter Test Guide PREFACE ........................................................................................................................................................... 2 ABOUT THE AUTHOR ........................................................................................................................................ 3 TESTING IN SOFTWARE PROJECTS .................................................................................................................... 5 WHY TEST AT ALL? TYPES OF TESTS NAMING STRATEGIES CODE COVERAGE BEST PRACTICES 5 5 6 7 7 8 UNIT TESTS ....................................................................................................................................................... 9 FLUTTER TESTING CONSTRAINTS CLUSTERING TESTS SETUP AND TEARDOWN CODE EXAMPLE TESTING THE INC() METHOD TESTING THE DEC() METHOD CONCLUSION 9 10 11 11 11 12 13 WIDGET TESTS ................................................................................................................................................ 14 THE APP TO TEST WRITING THE TESTS CONCLUSION 14 15 17 DEPENDENCY MOCKING ................................................................................................................................. 18 THE CLASS TO TEST GENERATING THE MOCK USING THE MOCK IN TESTS DEALING WITH LEGACY APPS MOCKITO CHEAT SHEET CONCLUSION 18 19 20 20 21 22 MEASURING CODE COVERAGE ....................................................................................................................... 23 THE TEST SUBJECT THE UNIT TESTS COLLECT COVERAGE INFORMATION SHOW COVERED LINES IN THE EDITOR SHOW COVERAGE INFORMATION PER FILE CONCLUSION 23 24 24 25 26 26 CLOSING WORDS ............................................................................................................................................ 27 4 Flutter Test Guide Testing in software projects Testing is an essential task in every software project (or at least it should be one). In this section, we’ll talk about some basic knowledge that every developer should have about testing. It’s about best practices, naming conventions, testing strategies, and what type of test is used in which situation. Why test at all? As soon as you compile and run your application, you have made the first test even if you didn’t intend to. So if you ever hear somebody say they don’t do any testing at all, you know better now. Every software developer does testing. The question is just how much they invest into it. Testing won’t guarantee bug-free software, but testing will increase your chances to find errors in your code. More tests don’t necessarily lead to more findings. Testing quality and strategy are essential factors for a flawlessly running app. For example, it doesn’t make sense (and it’s also not possible) to test any imaginable input of a text field. It‘s better to focus on edge cases like an empty string, numbers, special characters, and so on. The context is important and the developer has to identify these edge cases when writing tests. Types of tests Tests can roughly be divided into two types: manual and automatic tests. Both have their pros and cons that we’ll look into now. 👨 Manual tests A manual test is any test that involves human interaction. Compiling the code and running the app is the most basic one. Even complete use cases (launch the app, open the login page, insert email and password, press button, …) are sometimes tested repeatedly by dedicated testers. This is very expensive but sometimes it’s the reality because of a lack of knowledge, environmental constraints, or management errors. 💻 Automated tests Automated tests consist of unit tests, widget tests, and integration tests in the Flutter environment. They are usually executed by a build server and don’t require additional interaction. This reduces maintenance costs and overall testing time. In general, you should aim to automate as many tests as possible. But what type of automated tests are useful in what context? Use unit tests to test a single function, method, or class. Use widget tests to test a single widget, and use integration tests to test an entire app or large parts of it. The following table compares different types in various categories. 5 Flutter Test Guide Speed Dependencies Maintenance cost Code covered Unit Very fast Very little Very low Very little Widget Fast Little Normal Little Integration Normal Normal High Normal Manual Very slow Very much Very high Very much Comparison of different test types in various categories When you write tests according to best practices, you‘ll end up with lots of unit tests, fewer widget tests, even fewer integration tests, and hopefully no manual tests. The impact grows with every category but so do maintenance costs and required dependencies. A good balance between these categories is required and with more experience, you’ll figure it out eventually. Naming Naming tests is an important task to write maintainable code. A good naming convention allows other developers to quickly recognize the purpose of a test. Therefore, a lot of patterns have been created over the years. I’ll introduce the most common ones. You can use them or come up with your own variant. Being consistent and understandable is the key. Given When Then The Given When Then pattern is used a lot in Behavior-Driven Development. It defines preconditions (Given), an action (When), and a result (Then). This leads to very long, but easily understandable test names. ▶ GivenDatabaseIsUp_WhenUserInputIsValid_ThenUserIsCreated ▶ given_input_is_valid_when_button_is_clicked_then_value_is_converted_to_currency ▶ givenFilterIsNotEmptyWhenFilterButtonIsClickedThenMatchingItemsAreReturned When Then The When Then pattern is similar to the Given When Then pattern but omits any preconditions. Names will be shorter, but most likely not as understandable. ▶ WhenUserInputIsValid_ThenUserIsCreated ▶ when_button_is_clicked_then_value_is_converted_to_currency ▶ whenFilterButtonIsClickedThenMatchingItemsAreReturned Should The Should pattern gives a hint of what a test should check for. ▶ ShouldCreateUserInDatabase ▶ should_convert_to_currency ▶ shouldReturnMatchingItems Should When The Should When pattern indicates the outcome of a test by including a condition. It’s more verbose than the Should approach and tends to be more understandable. 6 Flutter Test Guide ▶ ShouldCreateUserInDatabaseWhenInputIsValid ▶ should_convert_to_currency_when_input_is_valid ▶ shouldReturnMatchingItemsWhenConditionIsFulfilled Verify The Verify approach tells a user what the outcome of a test should be. ▶ VerifyCreateUserInDatabase ▶ verify_input_convertion_to_currency ▶ verifyMatchingItems Descriptive sentence Use a descriptive sentence to indicate what a test should do. Use camel-case notation or separate words with underscores. ▶ CreateUserInDatabase ▶ convert_input_to_currency ▶ findMatchingItems Strategies There are two common testing strategies: white-box testing and black-box testing. A white-box test involves the analysis of the source code to write a test. In general, unit tests are white-box tests because they test small entities like classes or functions. White-box tests are usually written by developers only and are very low-level. During black-box testing, the developer doesn’t care about the source code. These tests define inputs and outcomes. The input is passed on to the testing entity and any result will be compared to the specified outcome which was defined beforehand. Integration and manual or system tests work mostly according to this pattern. Black-box tests are more highlevel and can be defined by stakeholders, requirements engineers, or any person on a project team. It doesn’t need to be a developer. Code coverage Code coverage tools can measure which code parts have been executed by tests. This helps us finding untested areas. It doesn’t tell us if the test is good or not. It is also not guaranteed that a covered part is bug-free. Code coverage is a helper to identify untested code parts, nothing more and nothing less. Some people believe that high coverage of the code base improves quality. But from my experience, I can tell that metrics like 80% overall coverage don’t lead to fewer bugs. It sometimes just makes the developer write a test to fulfill the coverage goal. Let’s have a look at a code example to learn how we can extract unit tests. The code snippet is below: 7 Flutter Test Guide double _calcPrice(String item, int amount, String subType) { if (item.isEmpty) { throw Exception("no item provided"); } switch (item) { case "cpu": return 99.99 * amount; case "graphics": if (subType.contains("amd") || subType.contains("nvidia")) { return 199.99 * amount; } if (subType.contains("intel")) return 99.99 * amount; throw Exception("unknown graphics card"); default: throw Exception("unknown item"); } } This function calculates a price based on given arguments. First, we handle branch coverage. This means our tests cover every branch. For if-conditions, we need two tests. For switch statements, we need one test per case branch plus one for the default branch. Next, we examine statement coverage. This means every statement is executed at least once. With our previously found tests, all statements are already covered. So we don’t need to add any further tests. Lastly, we focus on condition coverage. This means every condition is checked for every possible outcome. Our previous tests already do that, with one exception: The line if (subType.contains("amd") || subType.contains("nvidia")) needs three tests: ▶ subType.contains("amd") is true, subType.contains("nvidia") is false ▶ subType.contains("amd") is false, subType.contains("nvidia") is true ▶ subType.contains("amd") and subType.contains("nvidia") are both false This is how you identify tests to fulfill coverage. As you can see, some tests are redundant and apply to multiple coverage categories. But there are still some tests missing. For example, it‘s possible to pass a negative number as amount argument which leads to a negative price. This bug can‘t be prevented by just having 100% overall test coverage. Make sure to think of edge cases! Best practices Here are some best practices to write good unit tests. Keep your tests short One should always see what a test does at first glance. If scrolling is necessary or many method calls are involved, it gets messy. Make your test independent of other tests A test should always work in the same way. So make sure to set up data properly (and even reset them after the test if needed). Write tests while writing code Don’t skip the tests when you start. It is more likely that you will end up with untestable code or that you won’t test everything if you don’t do it while writing the code parts. 8 Flutter Test Guide Unit Tests We now know about the basics of testing in software products. In this chapter, we will focus on writing test cases as unit tests and how to structure them. You’ll learn the basics of unit testing in Flutter applications. It will give you a compact overview to start with. A link to all sources is provided at the end. This is the class we are going to write unit tests for. class CounterService { int _counter = 0; int get counter => _counter; final int? maxCounterValue; CounterService({this.maxCounterValue}); /// Increases the counter value if [maxCounterValue] has not been reached yet. CounterService inc() { if (maxCounterValue == null || _counter < maxCounterValue!) _counter++; return this; } /// Decreases the counter value if [counter] is bigger than zero. CounterService dec() { if (_counter > 0) _counter--; return this; } } This class manipulates a counter value when the methods are called. There are some conditions to be considered and a maximum value which the counter can’t exceed. Flutter testing constraints The Flutter test runner expects all files containing tests to end with the suffix _test.dart. In addition, they must be located in the test folder of a Flutter project structure. They are executed with the command flutter test or with special buttons in various IDEs and code editors. Flutter test support in Visual Studio Code. The red arrows indicate options to run, debug, and find tests in a Flutter project. 9 Flutter Test Guide Project structure with a test folder and a test file in Visual Studio Code Clustering tests The easiest way to cluster tests is by putting them in separate files. It is a common practice to have a test file for every code file. You’ll notice that in the upcoming example which uses a class counter_service.dart and its corresponding test class counter_service_test.dart. The test package of the Flutter SDK offers the possibility to define test groups. A test group is a set of tests with a common description which can be run together. Groups can also be nested if needed. It looks like this: void main() { group("group1", () { test("test1", () {}); test("test2", () {}); group("inner group", () { test("test3", () {}); }); }); } 10 Flutter Test Guide There is the additional option to define tags. A tag is a custom identifier for a test that can be interpreted by the test runner. You can include or exclude specific tags from your test run which is useful if you are testing platform-specific code like different operating systems (iOS, Android) or browsers (Chrome, Firefox). ➤ Documentation and example code for tags can be found on GitHub. Setup and teardown Sometimes, tests need a setup like creating objects or copying files to specific locations. For these cases, there are also methods in the test package. ▶ setUp() ▶ setUpAll() ▶ tearDown() ▶ tearDownAll() ➡ runs once before every test ➡ runs once before any test of the suite or group ➡ runs once after every test ➡ runs once after all tests of the suite or group They all expect a function as an argument and can be used at the top level or in groups (see the previous section). Code example Let’s assume your class to test is called counter_service.dart and sits under the lib folder in your project. Your matching test class will be called counter_service_test.dart and will be in the test folder structure. If you follow this convention the Flutter test runner can find and execute your tests. It can be triggered by the command flutter test while you are in the root folder of your project. Testing the inc() method First of all, we test the inc() method. Our goal is to execute every code branch which results in the following three tests. Validate that the counter is increased if there is no maxCounterValue set. Validate that the counter is increased if maxCounterValue is not reached yet. Validate that the counter is not increased if maxCounterValue is reached. test("Verify counter can be increased", () { final _sut = CounterService(); _sut.inc().inc().inc(); expect(_sut.counter, equals(3)); }); test("Verify counter can be increased if below max value", () { final _sut = CounterService(maxCounterValue: 2); _sut.inc().inc(); expect(_sut.counter, equals(2)); }); test("Verify counter cannot be increased above max value", () { final _sut = CounterService(maxCounterValue: 0); _sut.inc(); expect(_sut.counter, equals(0)); }); 11 Flutter Test Guide Every test creates an instance of the class being tested (System Under Test, that’s why I call the variable _sut), manipulates data, and checks the results. Verification of results is done by the expect function, which takes the actual value and a Matcher to evaluate the outcome. Here are some examples of how to work with matchers. test("Some matcher examples", () { // type check expect(CounterService(), isA<CounterService>()); // null check expect(null, isNull); expect("null", isNotNull); // bool check expect(3 == 3, isTrue); expect(3 != 3, isFalse); // equality check expect("1", equals("1")); // sign check expect(-1, isNegative); expect(1, isPositive); expect(0, isNonNegative); expect(0, isNonPositive); // list checks expect([], isEmpty); expect([2], isNotEmpty); }); Testing the dec() method This method is simpler as we only need two tests to cover all possible outcomes. Validate that the counter can be decreased Validate that the counter cannot be decreased below zero test("Verify counter can be decreased", () { // Arrange final _sut = CounterService(); _sut.inc().inc().inc(); expect(_sut.counter, equals(3)); // Act _sut.dec(); // Assert expect(_sut.counter, equals(2)); }); test("Verify counter cannot be decreased below zero", () { final _sut = CounterService(); _sut.dec(); expect(_sut.counter, equals(0)); }); As you can see every test follows the Arrange-Act-Assert pattern. It helps to structure the tests and is one of the most common strategies to write test code. To execute the tests, run the command flutter test. The test results will be displayed in the console window. Many IDEs or editors support running tests as well. 12 Flutter Test Guide Conclusion In this chapter, we focused on unit testing in Flutter applications. You learned about test organization, writing tests, and executing them with Visual Studio Code or the command line. In combination with the code examples, you should be able to create tests for your application. ➤ You can find the source code on GitHub. 13 Flutter Test Guide Widget tests In this section, we’ll focus on widget tests. This means that a part of the app is rendered during the tests and you can describe test steps to copy the behavior of a real user. The Flutter SDK offers methods to simulate a complete workflow. The app to test I created a simple app that needs some widget tests. Here’s a demo. Demo Flutter application to be tested The app contains a Scaffold with a static AppBar, a description Text, a ListView, and a FloatingActionButton. When you click on the button, a new row is inserted into the ListView with a random number between 0 and 10, the header text is updated, and a SnackBar is shown. You can also delete entries with a long press. A simple tap shows a confirmation of what row you clicked. This is what we are going to test now. 14 Flutter Test Guide Writing the tests As the app workflow is pretty simple, we’ll write just one test to fulfill all requirements. You can, of course, split your if it makes sense. Good test organization is essential in larger apps, so take your time and think about what’s reasonable. The tests use action steps simulating user input or actions and verification steps verifying if the outcome matches our expectations. Our test will do the following: Verify that there are already 3 data items present Tap the FloatingActionButton Verify that a new data item has been added Tap the 3rd item Verify that the SnackBar shows the same data as in the list Long press the 2nd item Verify if the correct item has been deleted The test skeleton for widget tests is similar to unit tests that you have seen in the previous chapter but has an additional WidgetTester argument. It always looks like this import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('Test description', (WidgetTester tester) async { }); } WidgetTester The WidgetTester is the central unit performing actions during a test. For example, there are methods to ▶ write into TextField widgets → enterText ▶ tap widgets → tap ▶ trigger animations and rendering → pump, pumpAndSettle Finding widgets To find widgets, use the Finder class. It has many methods to find exactly what you are looking for. Some examples are ▶ find.byType ▶ find.byKey ▶ find.text 15 Flutter Test Guide Auto-completion options of the Finder class in VS Code Verification For verification, use the expect library like in regular unit tests. A typical expect statement can look like this: expect(find.byText(”test”), findsOneWidget); Look for one or more specific widgets and assert it. Putting it all together The general workflow in widget tests is always the same. First, you use a Finder to identify a widget you want to interact with. Then, you pass it to the WidgetTester to perform an action and trigger animation as well as rendering. In the end, you verify the outcome via the expect class. Here’s a test example. The entire app source code will be linked at the end. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_widget_test/list_widget.dart'; import 'package:flutter_widget_test/main.dart'; void main() { testWidgets('Test app workflow', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(const MyApp()); // Verify that 3 data items exist // Here we look for 3 ListWidget elements which are the widgets that are // used in the ListView. expect(find.byType(ListWidget), findsNWidgets(3)); // Tap the FloatingActionButton // Here we use a Type Finder to find our item. Works great if there is only // one element of a type present. await tester.tap(find.byType(FloatingActionButton)); await tester.pump(); // Verify that a new row has been added // We again look for 4 ListWidget elements. Furthermore, we look for a // SnackBar and the matching text that is displayed when a new data item is // added. expect(find.byType(ListWidget), findsNWidgets(4)); 16 Flutter Test Guide expect(find.byType(SnackBar), findsOneWidget); expect(find.text("item 4 added"), findsOneWidget); // Tap 3rd item in the list // We use a Key Finder to identify the ListWidget we want to tap. Keys are // always unique and a good way to find the exact widget. The pumpAndSettle // is required because the previous SnackBar auto-dismisses after 1 second // and we need to wait for that to happen. await tester.tap(find.byKey(const Key("ListWidget2"))); await tester.pumpAndSettle(const Duration(milliseconds: 1250)); // Verify shown data is correct // We look for the SnackBar and a certain text. expect(find.byType(SnackBar), findsOneWidget); expect(find.text("item 3 pressed"), findsOneWidget); // Long press 2nd item // Again a Key Finder and a pumpAndSettle to wait for the previous SnackBar. await tester.longPress(find.byKey(const Key("ListWidget1"))); await tester.pumpAndSettle(const Duration(milliseconds: 1250)); // Verify pressed item was deleted // Finding widgets by type, by key, or by text. There are many possibilities. expect(find.byType(ListWidget), findsNWidgets(3)); expect(find.byKey(const Key("ListWidget0")), findsOneWidget); expect(find.byKey(const Key("ListWidget1")), findsOneWidget); expect(find.byKey(const Key("ListWidget2")), findsOneWidget); expect(find.byKey(const Key("ListWidget3")), findsNothing); expect(find.byType(SnackBar), findsOneWidget); expect(find.text("item 2 deleted"), findsOneWidget); }); } Conclusion In this chapter, we had a look at Widget testing in Flutter applications. You learned about the WidgetTester, how to perform actions, and how to verify the outcome with code examples. You should be capable of constructing a useful testing procedure for your apps now. ➤ You can find the source code on GitHub. 17 Flutter Test Guide Dependency mocking During testing, there will always be a moment when you need a mock. This will save you from setting up complex environments for a test as it allows you to focus on the parts you want to test. Assume you got a logger in multiple places of your application that writes into a database. Sure enough, you have tests for your logger to ensure its correct functionality. But when you test e. g. your login mechanism, you don’t care about the logger. Here’s when a mock comes in handy. It handles the calls to the logger and reacts the way it was configured. In this section, we are looking at the Mockito package for Flutter apps. It allows you to create mocks so that any dependencies of your classes don’t need to be handled for testing. The class to test This is the DataService class we are going to write tests for. It takes a DataStore object as a constructor argument. The DataStore can be anything from a remote database over local device storage to a simple Map<string, dynamic>() object that stores the values. We don’t want to deal with this dependency, so we are going to mock it in our test. import 'data_store.dart'; class DataService { final DataStore store; DataService(this.store); dynamic get(String identifier) { return store.get(identifier); } void add(String identifier, dynamic data) { store.add(identifier, data); } void update(String identifier, dynamic data) { store.update(identifier, data); } void remove(String identifier) { store.remove(identifier); } } Our class to be tested 18 Flutter Test Guide Generating the mock The build runner tool of Dart helps to create mocks for every class we want. We add it to our Flutter app via the command flutter pub add build_runner. Afterward, we specify classes for which a mock should be generated. We add an annotation to the main() method of our empty test class (see code below). @GenerateMocks([DataStore]) void main() {} Annotation to create mocks by the build runner tool And in the end, we call the build runner with flutter pub run build_runner build. It will create a new class containing all mocks specified in the array of the annotation. Build runner output Generated class with mocks 19 Flutter Test Guide Using the mock in tests To test the get() method, we need to set up the mock. Otherwise, an exception will be thrown, because since Dart 2.12/Flutter 2 and Null Safety there isn’t a common default value like null which could be returned. To specify the behavior, we use the when() and thenAnswer() methods: test("Verify get returns values", () { var storeMock = MockDataStore(); var service = DataService(storeMock); when(storeMock.get(any)).thenAnswer((_) => "I am a value!"); expect("I am a value!", service.get("dummy")); verify(storeMock.get(any)).called(1); }); Mock with setup Whenever the get() method of the mock is called, it answers with a fixed value which we can verify afterwards. Additionally, we can ask the mock if the get() method was called to be more confident. Dealing with legacy apps ❗ The following hint is not the recommended behavior and should only be used for legacy apps without Null Safety support. If you want your mocks to return null when no setup is available, then replace your annotation @GenerateMocks([DataStore]) with @GenerateMocks([],customMocks: [MockSpec<DataStore>(returnNullOnMissingStub: true)]) Your test looks like this test("Verify get returns values", () { var storeMock = MockDataStore(); var service = DataService(storeMock); service.get("dummy"); verify(storeMock.get(any)).called(1); }); Mock with a default return value of null 20 Flutter Test Guide Mockito Cheat Sheet Here is a cheat sheet for the Mockito package used to mock dependencies in the previous chapter. It shows various options to set up mocks and verify interactions. test("Some tests with mockito", () async { ////////////////////////////////// // --- Define return values --- // ////////////////////////////////// // Returns a Future without a value when(mock.addAsync(any, any)) .thenAnswer((realInvocation) => Future.value()); await sut.addAsync("2", "2"); verify(mock.addAsync(any, any)).called(1); // Returns a Future with a value when(mock.getAsync(any)).thenAnswer((realInvocation) => Future.value("42")); expect(await sut.getAsync("2"), "42"); verify(mock.getAsync(any)).called(1); ////////////////////////////// // --- Throw exceptions --- // ////////////////////////////// // Throws exception when(mock.removeAsync(any)).thenThrow(Exception()); when(mock.remove(any)).thenThrow(Exception()); expect(() => sut.remove("1"), throwsA(isA<Exception>())); expect(() async => await sut.removeAsync("1"), throwsA(isA<Exception>())); verify(mock.remove(any)).called(1); verify(mock.removeAsync(any)).called(1); ///////////////////////////////////// // --- Using argument matchers --- // ///////////////////////////////////// // Matches on any argument when(mock.get(any)).thenAnswer((realInvocation) => "42"); expect(mock.get(null) == "42", isTrue); // Matches on any argument of type string when(mock.get(argThat(isA<String>()))).thenAnswer((realInvocation) => "42"); expect(mock.get("") == "42", isTrue); // Matches on any named argument when(mock.updateAsync(any, data: anyNamed("data"))) .thenAnswer((realInvocation) => Future.value()); await mock.updateAsync("dummy", data: 123); verify(mock.updateAsync(any, data: anyNamed("data"))).called(1); ///////////////////////////////// // --- Capturing arguments --- // ///////////////////////////////// // Captures the arguments await mock.updateAsync("test", data: 321); expect( verify(mock.updateAsync(captureAny, data: captureAnyNamed("data"))) .captured, ["test", 321]); // Captures the arguments according to a condition await mock.updateAsync("test", data: 321); await mock.updateAsync("not a test", data: 322); await mock.updateAsync("testme", data: 323); expect( verify(mock.updateAsync(captureThat(startsWith("t")), data: captureAnyNamed("data"))) .captured, ["test", 321, "testme", 323]); ////////////////////////// 21 Flutter Test Guide // --- Verification --- // ////////////////////////// clearInteractions(mock); // Verifies the number of calls mock.get("111"); mock.get("222"); verify(mock.get(any)).called(2); mock.get("111"); mock.get("222"); verify(mock.get(any)).called(lessThan(3)); // Verifies a mock wasn't called with the given arguments verifyNever(mock.get("123")); // Verifies the order of call mock.get("get"); mock.add("add", "add"); mock.get("get"); verifyInOrder([mock.get(any), mock.add(any, any), mock.get(any)]); // Verifies that there weren't any more calls after the last verification verifyNoMoreInteractions(mock); reset(mock); // Verifies that the mock wasn't called at all verifyZeroInteractions(mock); /////////////////////// // --- Resetting --- // /////////////////////// // Removes all stubs, clears captured calls reset(mock); // Clears captured calls clearInteractions(mock); }); Final tips ▶ Always use thenAnswer(), never use thenReturn() Explanation ▶ If you verify a mock, the invocation counter is reset! ▶ If there are multiple matching setups, the last setup wins! Conclusion With this cheat sheet, you have all the tools needed to use mocking with Mockito. ➤ You can find the source code on GitHub. 22 Flutter Test Guide Measuring code coverage Now, let me show you how you can measure your code coverage and see the covered and uncovered lines in your Flutter app. We are going to use Visual Studio Code and two free extensions from the Visual Studio Marketplace: Flutter Coverage and Coverage Gutters. Flutter Coverage extension for Visual Studio Code to show code coverage for every file Coverage Gutters extension for Visual Studio Code to show covered and uncovered lines The test subject I created a simple class called login_service.dart. It allows logging in, logging out, and checking the current state (logged in or not). It’s very basic and has flaws (like hard-coded user name and password) but will be enough to demonstrate how coverage works. The class is shown below. class LoginService { final String _expectedPassword = "abc123!?="; final String _expectedUser = "superUser"; bool _isLoggedIn = false; void login(String? user, String? password) { if (_isLoggedIn) throw Exception("Please log out first"); if (user != _expectedUser) throw Exception("Wrong user name"); if (password != _expectedPassword) throw Exception("Wrong password"); _isLoggedIn = true; } void logout() { _isLoggedIn = false; } bool isLoggedIn() { return _isLoggedIn; } } 23 Flutter Test Guide The unit tests Furthermore, we have some unit tests to cover all possible statements of the LoginService: void main() { test("Verify failed login with wrong user", () { var _sut = LoginService(); expect(() => _sut.login("user", "abc123!?="), throwsException); expect(_sut.isLoggedIn(), isFalse); }); test("Verify failed login with wrong password", () { var _sut = LoginService(); expect(() => _sut.login("superUser", "password"), throwsException); expect(_sut.isLoggedIn(), isFalse); }); test("Verify logout before login", () { var _sut = LoginService(); _sut.login("superUser", "abc123!?="); expect(_sut.isLoggedIn(), isTrue); expect(() => _sut.login("superUser", "abc123!?="), throwsException); expect(_sut.isLoggedIn(), isTrue); }); test("Verify successful login", () { var _sut = LoginService(); _sut.login("superUser", "abc123!?="); expect(_sut.isLoggedIn(), isTrue); }); test("Verify logout works", () { var _sut = LoginService(); expect(_sut.isLoggedIn(), isFalse); _sut.login("superUser", "abc123!?="); expect(_sut.isLoggedIn(), isTrue); _sut.logout(); expect(_sut.isLoggedIn(), isFalse); _sut.logout(); expect(_sut.isLoggedIn(), isFalse); }); } Collect coverage information Flutter has a built-in command to collect coverage information while executing all tests. So you just need to open a shell of your choice, navigate to the project root folder, and execute the following command: flutter test --coverage This will execute all tests in your project and create a file lcov.info containing the coverage information. The extensions you installed earlier rely on the file to visualize the information. 24 Flutter Test Guide Show covered lines in the editor After the coverage information has been collected open an already tested file (in this example login_service.dart). You should see something like this. Covered lines by tests in Visual Studio Code A green line means there is a test executing this code line while a red line means that there is no test executing this code line. This class has a coverage of 100% as all unmarked lines (like field declarations) are not measured by the Flutter tool. ❗ Be aware that the colors don’t indicate if a test passes or not. They just tell you if there is any test for a line. If you can’t see any colorings check the status bar and enable the coverage gutters. Coverage display is not enabled, click to enable Coverage display is enabled, click to disable 25 Flutter Test Guide Show coverage information per file In bigger projects, you’d like to have an overview of the files having sufficient test coverage and those which don’t. To solve this problem you can use the Flutter Coverage extension. It shows the coverage percentage of every dart file in your project. Go to your Test Explorer in VS Code and open the Flutter Coverage drawer. You should see something like this. Test coverage per file with Flutter Coverage in VS Code The coverage of the login_service.dart class is 100% as we have concluded before. Especially if your project gets bigger and bigger this view will be quite handy to identify insufficiently tested classes. Conclusion With the extensions Flutter Coverage and Coverage Gutters, you are more likely to notice missing tests in your code base. They can play an important role in your testing process and will hopefully lead to fewer bugs in your application. ➤ You can find the source code on GitHub. 26 Flutter Test Guide Closing words Thank you for reading this test guide. I’ve put a lot of work into it and there will be future versions with additional content. I hope you gained valuable insights and new knowledge on your path to becoming a (better) Flutter developer. If you have any questions regarding a specific topic or feedback, don’t hesitate to contact me. Also, check my Gumroad page for other free stuff, ebooks, and digital content, and be sure to follow me on Medium to not miss any new articles and tutorials about Flutter. Image from freepik 27