Flutter 3.41 is live! Check out theblog post!
Testing each layer
How to test an app that implements MVVM architecture.
Testing the UI layer
#One way to determine whether your architecture is sound is considering how easy (or difficult) the application is to test. Because view models and views have well-defined inputs, their dependencies can easily be mocked or faked, and unit tests are easily written.
ViewModel unit tests
#To test the UI logic of the view model, you should write unit tests that don't rely on Flutter libraries or testing frameworks.
Repositories are a view model's only dependencies (unless you're implementinguse-cases), and writingmocks orfakes of the repository is the only setup you need to do. In this example test, a fake calledFakeBookingRepository is used.
voidmain(){group('HomeViewModel tests',(){test('Load bookings',(){// HomeViewModel._load is called in the constructor of HomeViewModel.finalviewModel=HomeViewModel(bookingRepository:FakeBookingRepository()..createBooking(kBooking),userRepository:FakeUserRepository(),);expect(viewModel.bookings.isNotEmpty,true);});});} TheFakeBookingRepository class implementsBookingRepository. In thedata layer section of this case-study, theBookingRepository class is explained thoroughly.
classFakeBookingRepositoryimplementsBookingRepository{List<Booking>bookings=List.empty(growable:true);@overrideFuture<Result<void>>createBooking(Bookingbooking)async{bookings.add(booking);returnResult.ok(null);}// ...}If you're using this architecture withuse-cases, these would similarly need to be faked.
View widget tests
# Once you've written tests for your view model, you've already created the fakes you need to write widget tests as well. The following example shows how theHomeScreen widget tests are set up using theHomeViewModel and needed repositories:
voidmain(){group('HomeScreen tests',(){lateHomeViewModelviewModel;lateMockGoRoutergoRouter;lateFakeBookingRepositorybookingRepository;setUp((){bookingRepository=FakeBookingRepository()..createBooking(kBooking);viewModel=HomeViewModel(bookingRepository:bookingRepository,userRepository:FakeUserRepository(),);goRouter=MockGoRouter();when(()=>goRouter.push(any())).thenAnswer((_)=>Future.value(null));});// ...});} This setup creates the two fake repositories needed, and passes them into aHomeViewModel object. This class doesn't need to be faked.
The code also defines aMockGoRouter. The router is mocked usingpackage:mocktail, and is outside the scope of this case-study. You can find general testing guidance inFlutter's testing documentation.
After the view model and its dependencies are defined, the Widget tree that will be tested needs to be created. In the tests forHomeScreen, aloadWidget method is defined.
voidmain(){group('HomeScreen tests',(){lateHomeViewModelviewModel;lateMockGoRoutergoRouter;lateFakeBookingRepositorybookingRepository;setUp(// ...);voidloadWidget(WidgetTestertester)async{awaittestApp(tester,ChangeNotifierProvider.value(value:FakeAuthRepository()asAuthRepository,child:Provider.value(value:FakeItineraryConfigRepository()asItineraryConfigRepository,child:HomeScreen(viewModel:viewModel),),),goRouter:goRouter,);}// ...});} This method turns around and callstestApp, a generalized method used for all widget tests in the compass app. It looks like this:
voidtestApp(WidgetTestertester,Widgetbody,{GoRouter?goRouter,})async{tester.view.devicePixelRatio=1.0;awaittester.binding.setSurfaceSize(constSize(1200,800));awaitmockNetworkImages(()async{awaittester.pumpWidget(MaterialApp(localizationsDelegates:[GlobalWidgetsLocalizations.delegate,GlobalMaterialLocalizations.delegate,AppLocalizationDelegate(),],theme:AppTheme.lightTheme,home:InheritedGoRouter(goRouter:goRouter??MockGoRouter(),child:Scaffold(body:body,),),),);});}This function's only job is to create a widget tree that can be tested.
TheloadWidget method passes in the unique parts of a widget tree for testing. In this case, that includes theHomeScreen and its view model, as well as some additional faked repositories that are higher in the widget tree.
The most important thing to take away is that view and view model tests only require mocking repositories if your architecture is sound.
Testing the data layer
# Similar to the UI layer, the components of the data layer have well-defined inputs and outputs, making both sides fake-able. To write unit tests for any given repository, mock the services that it depends on. The following example shows a unit test for theBookingRepository.
voidmain(){group('BookingRepositoryRemote tests',(){lateBookingRepositorybookingRepository;lateFakeApiClientfakeApiClient;setUp((){fakeApiClient=FakeApiClient();bookingRepository=BookingRepositoryRemote(apiClient:fakeApiClient,);});test('should get booking',()async{finalresult=awaitbookingRepository.getBooking(0);finalbooking=result.asOk.value;expect(booking,kBooking);});});} To learn more about writing mocks and fakes, check out examples in theCompass Apptesting directory or readFlutter's testing documentation.
Feedback
#As this section of the website is evolving, wewelcome your feedback!
Unless stated otherwise, the documentation on this site reflects Flutter 3.41.2. Page last updated on 2025-10-30.View source orreport an issue.