A production-ready Flutter testing suite built on top of a Clean Architecture app using FakeStoreAPI, Retrofit, Dio, Cubit, and GetIt — covering every testing layer from pure logic to real network calls.
Most Flutter developers build the app and skip the tests. This project does the opposite — it takes a complete Clean Architecture Flutter app and builds a full testing suite around it, covering every single layer with the right tool for that layer.
This is not a tutorial with toy examples. Every test here reflects a real pattern you would use in a production codebase.
UI (ProductPage)
↓
ProductCubit (flutter_bloc)
↓
GetProductsUseCase
↓
ProductRepository (abstract)
↓
ProductRepositoryImpl
↓
ProductRemoteDataSource
↓
ApiService (Retrofit + Dio) → https://fakestoreapi.com
Each layer only knows about the layer directly below it. The domain layer has zero Flutter or Retrofit dependencies.
lib/
├── main.dart
├── core/
│ ├── di/injection.dart
│ ├── errors/
│ │ ├── failures.dart
│ │ └── error_handler.dart
│ └── networks/
│ ├── api_service.dart
│ └── api_service.g.dart
└── features/products/
├── data/
│ ├── datasources/product_remote_datasource.dart
│ ├── mappers/product_mapper.dart
│ ├── models/
│ │ ├── product_response_model.dart
│ │ └── rating_model.dart
│ └── repositories/product_repository_impl.dart
├── domain/
│ ├── entities/product.dart
│ ├── repositories/product_repository.dart
│ └── usecases/get_products_usecase.dart
└── presentation/
├── pages/product_page.dart
└── state/
├── product_cubit.dart
└── product_state.dart
test/
├── unit/
│ ├── logic/price_calculator_test.dart
│ ├── repository/product_repository_test.dart
│ └── cubit/product_cubit_test.dart
├── api/
│ └── api_service_test.dart
├── widget/
│ └── product_page_test.dart
├── integration/
│ └── app_flow_test.dart
└── mocks/
├── mock_api_service.dart
├── mock_api_service.mocks.dart
├── mock_product_repository.dart
└── mock_product_repository.mocks.dart
Files marked with
.mocks.dartare auto-generated by build_runner. Do not edit them manually.
File: test/unit/logic/price_calculator_test.dart
Tests a pure Dart PriceCalculator utility with zero dependencies and zero mocks. Covers discount calculation, price formatting, cart totals with tax, and free shipping threshold logic. This is the simplest form of testing — input goes in, output comes out, you assert it matches.
File: test/unit/repository/product_repository_test.dart
Tests ProductRepositoryImpl as the error translator of the app. Wires up a real chain: MockApiService → ProductRemoteDataSourceImpl → ProductRepositoryImpl. The HTTP call is mocked using Mockito's @GenerateMocks and build_runner.
Covers:
- Success: correct mapping from raw model to domain entity
- Empty list handling
- 100 products without issues
- 401 Unauthorized → UnauthorizedFailure
- Connection timeout → TimeoutFailure
- Receive timeout → TimeoutFailure
- Send timeout → TimeoutFailure
- 500 Internal Server Error → ServerFailure
- 503 Service Unavailable → ServerFailure
- 404 Not Found → NotFoundFailure
- Connection error → NetworkFailure
- Retry simulation: fails twice, succeeds on 3rd attempt
File: test/unit/cubit/product_cubit_test.dart
Tests every state transition of ProductCubit using bloc_test and Mockito. The repository is mocked so no real data fetching happens.
Covers:
- Initial state is ProductInitial
- loadProducts() emits Loading then Loaded on success
- Loaded state contains correct number of products
- Loaded state with empty list
- Loading then Error on ServerFailure
- Loading then Error on NetworkFailure
- Loading then Error on UnauthorizedFailure
- Loading then Error on TimeoutFailure
- Loading then Error on unexpected exception
- Multiple calls emit Loading again each time
- Recovery: error on first call, success on second
- State equality via Equatable
File: test/api/api_service_test.dart
No mocks. Real HTTP calls against https://fakestoreapi.com using the actual ApiService built with Retrofit and Dio.
Covers:
- Returns non-empty list of ProductResponseModel
- Exactly 20 products returned
- Every product has id greater than 0, non-empty title, image, category
- Prices are positive doubles
- Image URLs start with https://
- Rating rate is between 0 and 5
- Rating count is non-negative
- All fields non-null on first product
- All categories belong to the 4 known FakeStore categories
- 1ms timeout throws DioException
- Fake domain simulating server down throws DioException
Requires active internet connection to run.
File: test/widget/product_page_test.dart
Tests ProductPage in complete isolation from all business logic. A MockProductCubit is injected via BlocProvider.value and frozen in a specific state using whenListen with initialState — so the cubit never actually runs any logic, it just holds a fixed state.
Important: bloc_test's MockCubit is built on Mocktail internally, not Mockito. This means you must use Mocktail's lambda syntax when(() => mock.method()) instead of when(mock.method()). Using Mockito syntax here causes a null crash because Mockito calls the method immediately during stub registration before any stub exists.
Covers:
- ProductLoading → Shimmer visible, product list hidden
- ProductLoaded → correct Cards render, titles and prices visible
- ProductLoaded with empty list → no error view shown
- ProductError → error view, message text, retry button visible
- Tapping retry calls loadProducts on the cubit
- AppBar always shows ShopFlow
File: test/integration/app_flow_test.dart
The closest to a real end-to-end test. Wires up the full real stack: real GetProductsUseCase, real ProductCubit, real ProductPage. Only the repository boundary is mocked using Mockito's generated MockProductRepository. The actual BLoC state machine runs during the test.
Covers:
- App launches → Shimmer visible → products appear after settle
- Product titles and formatted prices visible after load
- Error screen appears when repository throws
- Retry button tap: error screen → products appear
- Shimmer disappears after data loads
- AppBar always visible regardless of state
- Empty list shows product_list key, not error_view
The biggest lesson in this project is that different layers need different mocking strategies.
For repository and API mocks we use Mockito with @GenerateMocks and build_runner. Repository methods like Future getProducts() have non-nullable return types. If you write a mock by hand, noSuchMethod returns null which crashes. Mockito's code generator reads the real class and produces a perfectly null-safe mock with a safe default returnValue for every method.
For Cubit mocks in widget tests we use Mocktail because bloc_test's MockCubit is built on Mocktail internally. Mocktail solves the null crash differently — instead of code generation it uses a lambda so the method is never actually called during stub registration, making it inherently null-safe without any build step.
git clone https://github.com/your-username/flutter_testing_masterclass.git
cd flutter_testing_masterclassflutter pub getdart run build_runner build --delete-conflicting-outputsflutter testflutter test test/unit/logic/price_calculator_test.dart
flutter test test/unit/repository/product_repository_test.dart
flutter test test/unit/cubit/product_cubit_test.dart
flutter test test/widget/product_page_test.dart
flutter test test/integration/app_flow_test.dart
# Requires internet
flutter test test/api/api_service_test.dartdependencies:
dio: ^5.4.0
retrofit: ^4.9.2
json_annotation: ^4.9.0
get_it: ^7.7.0
flutter_bloc: ^8.1.6
equatable: ^2.0.5
cached_network_image: ^3.3.1
shimmer: ^3.0.0
google_fonts: ^6.2.1
dev_dependencies:
build_runner: ^2.4.9
retrofit_generator: ^10.2.5
json_serializable: ^6.8.0
mockito: ^5.4.4
mocktail: ^1.0.0
bloc_test: ^9.1.7- How to test every layer of a Clean Architecture Flutter app
- Why Mockito needs code generation for null safety and how it works
- Why bloc_test uses Mocktail and how the lambda syntax prevents null crashes
- How noSuchMethod works as the invisible engine powering all mock behaviour
- How to simulate real network errors: 401, 500, timeout, server down, retry
- How to freeze a Cubit in a specific state for UI testing using whenListen
- How to wire up a real BLoC state machine in an integration test
- The difference between stubbing with when and verifying with verify
Built as part of a daily Flutter learning series focused on production-level architecture, clean code, testing, and real-world patterns.
If this helped you, give it a star on GitHub ⭐