Friends Roster – Testing In Flutter
This is the continuation of “how I built the Friends Roster app” saga. If you missed the previous parts in this series, do check them out — part 1, part 2, part 3, part 4, part 5, part 6, part 7 and part 8. So far we have seen the code that runs the app. What about testing? Normally when I work on these hobby projects I don't write any tests (gasp!). I know. I sometimes did feel the need for tests, especially if it is a large project with lots of moving parts. They help you when making changes to the code. You can be confident that your changes will not unexpectedly break something.
When I started this project I wanted to check the testing capabilities of Flutter. It is a small project, so should be easy to write tests. Flutter has very good testing support. I could write unit tests, widget tests, integration tests and even screen diffs. They came very handy when making code changes.
Unit tests
These are the lightest tests to write. The setup was simple and you don't have to bring up any UI related framework code. Some examples below --
test('Check friend update from old data', () { Friend friend = Friend(FRIEND_ID, FRIEND_NAME, FRIEND_PHONE, []); final Map<String, dynamic> data = { 'name': FRIEND_NAME, 'phone': FRIEND_PHONE, 'call_history': [] }; expect(friend.toJson(), equals(data)); CallInfo callInfo1 = CallInfo(date1, CallState.CALLED); CallInfo callInfo2 = CallInfo(date2, CallState.MISSED); final Friend anotherFriend = Friend(FRIEND_ID, FRIEND_NAME, FRIEND_PHONE, [ callInfo1, callInfo2, ]); friend.updateFrom(anotherFriend); data['call_history'].add(callInfo1.toJson()); data['call_history'].add(callInfo2.toJson()); expect(friend.toJson(), equals(data)); });
test('Check details route maps to /details', () async { FriendsRouteInformationParser parser = FriendsRouteInformationParser(); RouteInformation routeInfo = parser.restoreRouteInformation(FriendsRoutePath.details('37')); expect(routeInfo.location, '/details/37'); });
Widget tests
These tests test more functionality then unit tests and need some setup. For example I had to setup mock Firebase responses so I could test the app storing data into Firestore, or authenticating with Google etc. I used mockito for this purpose and clock to move back and forth in time in the tests without actually changing the system time. Here are some widget tests
initFirebaseAndSetupFirestoreFriends( WidgetTester tester, List<FriendInfo> friends) async { String uid = 'uid'; setupFirebaseMocks(); await Firebase.initializeApp(); setupFirebaseUser(uid: uid); final firestoreUserData = setupFirestoreUser(tester, uid, true); MockFirestore mockFirestore = firestoreUserData['store']; MockDocumentReference mockUserDocRef = firestoreUserData['doc']; MockWriteBatch mockWriteBatch = new MockWriteBatch(); when(mockFirestore.batch()).thenReturn(mockWriteBatch); return setupFirestoreFriendsForList(mockFirestore, mockUserDocRef, friends: friends); } testWidgets('Shows friend who was called more than 3 month ago', (WidgetTester tester) async { await withClock(Clock(() => DateTime.parse(FAKE_DATE_STR)), () async { await _setupWidget(tester, remoteFriends: [ RECENTLY_CALLED_FRIEND, CALLED_MORE_THAN_3_MONTHS_FRIEND ]); expect(find.text( CALLED_MORE_THAN_3_MONTHS_FRIEND.friendSnap['name']), findsOneWidget); }); });
Screen diff tests
When making UI changes, these tests helped me quite a bit. Basically the tests check the current UI with a set of golden images and make sure the UI has not changed. Helps when making some changes to a widget (padding for example) effecting some other part of the UI. Also useful to catch responsive layout bugs. For example a UI change for small screen might inadvertently change a large screen layout.
testWidgets('Golden test for call page android', (WidgetTester tester) async { tester.binding.window.physicalSizeTestValue = Size(600, 800); tester.binding.window.devicePixelRatioTestValue = 1.0; await _setupWidget(tester); await expectLater(find.byType(MaterialApp), matchesGoldenFile('golden/android_call_page.png')); }); testWidgets('Golden test for call page web', (WidgetTester tester) async { tester.binding.window.physicalSizeTestValue = Size(1200, 800); tester.binding.window.devicePixelRatioTestValue = 1.0; Dependency.isWeb = true; await _setupWidget(tester); await expectLater(find.byType(MaterialApp), matchesGoldenFile('golden/web_call_page.png')); Dependency.isWeb = false; });
Conclusion
Writing tests was a breeze and I really enjoyed writing tests. While it did slow my development a bit, I am glad I have tests. I wrote 153 tests and it only takes about 30 seconds to run all of them. My test code coverage is 99.6%. So I am confident to make code changes in future. You can run tests and generate a code coverage report by running the following commands.
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
Some useful testing tips are available in this video if you like