Skip to content

ProgramTechBro/flutter_testing

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Flutter Testing Masterclas

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.


What This Project Is

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.


Architecture

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.


Project Structure

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.dart are auto-generated by build_runner. Do not edit them manually.


Testing Layers

Unit — Pure Logic

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.


Unit — Repository Layer

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

Unit — Cubit State Testing

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

API — Real Network Tests

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.


Widget — UI Isolation Testing

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

Integration — Full App Flow

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

Key Concept: Why Different Tools for Different Layers

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.


Setup

1. Clone the repository

git clone https://github.com/your-username/flutter_testing_masterclass.git
cd flutter_testing_masterclass

2. Install dependencies

flutter pub get

3. Generate mocks and Retrofit code

dart run build_runner build --delete-conflicting-outputs

4. Run all tests

flutter test

5. Run individual test files

flutter 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.dart

Dependencies

dependencies:
  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

What You Will Learn From This Project

  • 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

Author

Built as part of a daily Flutter learning series focused on production-level architecture, clean code, testing, and real-world patterns.


Support

If this helped you, give it a star on GitHub ⭐

About

Complete Flutter Testing Suite — Unit, Widget, Integration & API testing using Mockito, Mocktail, bloc_test and Dio on a Clean Architecture app with FakeStoreAPI

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages