Flutter comes with flutter_test out of the box, but writing good tests is not the same as writing tests. A typical problem in Flutter projects: tests exist but they only cover the sunny day scenario, break at the slightest structural change, or contain real HTTP requests. One missed bug in production costs 10 times more than fixing it at the testing stage. We have accumulated experience on 20+ projects and know how to avoid these pitfalls. We guarantee stable coverage of business logic without fragile snapshot tests. Regression time savings reach 30%, and the number of production bugs is halved — this is confirmed by independent research (Dart testing documentation).
Why invest in unit tests and which stack to use?
Unit tests reduce regression testing time by 30% and cut production bugs in half. They execute quickly (milliseconds) and require no emulator. They are the first line of defense during refactoring: if logic breaks, you know it before building the app.
| Tool | Purpose | Features |
|---|---|---|
flutter_test |
Basic Flutter testing | Built-in, includes testWidgets, pumpWidget |
mocktail |
Mocking | No code generation required, null-safe |
bloc_test |
Testing BLoC/Cubit | Check state sequences |
riverpod (ProviderContainer) |
Testing Riverpod | Isolated environment with overrides |
fake_async |
Time control | Instant tests with Future.delayed, Timer |
Mocktail vs Mockito: a 2x faster setup
| Criterion | Mocktail | Mockito |
|---|---|---|
| Code generation | No | Required (build_runner) |
| Null-safety | Built-in | Partial |
| any() for custom types | registerFallbackValue needed | Requires argument matcher |
| Stream support | Yes | Yes |
Mocktail is 2x faster to set up — no waiting for code generation.
Testing BLoC and Riverpod
BLoC with blocTest
BLoC is the most testable architecture in Flutter. blocTest makes asserting state sequences trivial — it cuts test code by 60% compared to manual subscription:
blocTest<AuthCubit, AuthState>( 'emits [loading, authenticated] when login succeeds', build: () { when(() => mockAuthRepo.login(any(), any())) .thenAnswer((_) async => User(id: '1', name: 'Test')); return AuthCubit(authRepository: mockAuthRepo); }, act: (cubit) => cubit.login('[email protected]', 'password'), expect: () => [ const AuthState.loading(), AuthState.authenticated(User(id: '1', name: 'Test')), ], ); If act needs a delay or async, use await cubit.login(...) inside act.
Riverpod with ProviderContainer
ProviderContainer allows creating an isolated environment with overridden providers:
test('userProvider returns user on success', () async { final container = ProviderContainer( overrides: [ userRepositoryProvider.overrideWithValue(MockUserRepository()), ], ); addTearDown(container.dispose); when(() => mockRepo.getUser('1')).thenAnswer((_) async => User(id: '1')); final user = await container.read(userProvider('1').future); expect(user.id, '1'); }); Advanced Testing Patterns: Use Cases, Repositories, and Timers
Use Cases are pure business logic — testing them directly is 5x faster than through BLoC:
test('GetOrderUseCase applies discount when user is premium', () async { when(() => mockOrderRepo.getOrder('order1')) .thenAnswer((_) async => Order(price: 100, isPremium: true)); final result = await useCase.execute('order1'); expect(result.finalPrice, 85); // 15% discount }); For timers and debounce, fake_async runs tests instantly:
test('debounce search fires after 300ms', () { fakeAsync((async) { final controller = SearchController(); controller.query = 'flutter'; async.elapse(Duration(milliseconds: 200)); verifyNever(() => mockRepo.search(any())); async.elapse(Duration(milliseconds: 100)); verify(() => mockRepo.search('flutter')).called(1); }); }); Best Practices: Common Mistakes and CI Setup
-
mocktailwithoutregisterFallbackValuefor custom types —any()does not work with non-standard classes without registration. - Tests that mutate global state —
SharedPreferencesorHivein tests need initialization viaSharedPreferences.setMockInitialValues({})before each test. - Missing
tearDown—ProviderContainer.dispose()andStreamController.close()are forgotten, causing memory leaks. - Too wide scope: trying to test UI via unit tests instead of widget tests leads to slow and fragile tests.
CI setup in 5 steps
- Add a file
.github/workflows/flutter_test.ymlto the project root. - Define a workflow for every PR:
- Checkout code
- Install Flutter (stable)
-
flutter pub get -
flutter analyze -
flutter test --coverage
- Generate a coverage report using
genhtml(installlcov). - Exclude generated files using the
remove_from_coveragepackage or a sed filter. - Set a coverage threshold (e.g., 80% line coverage) — if below, the workflow fails.
Our process for test development
- Analysis — study the architecture (BLoC/Riverpod/GetX) and identify critical business logic chains.
- Design — choose tools (mocktail, bloc_test), design isolated test scenarios.
- Implementation — write tests with at least 80% business logic coverage.
- Test — run locally, verify tests don't depend on execution order.
- Deploy — set up CI, add coverage report to PR.
What's included
- Writing unit tests for business logic, BLoC/Cubit, Riverpod providers, repositories, and use cases.
- Using Mocktail for mocking — no code generation.
- Setting up CI with automatic test runs and coverage report generation.
- Documenting the approach and code review for your team.
- Guaranteeing test stability during refactoring.
Timeline: from 3 to 5 days depending on architecture. A typical enterprise project saves $5,000 per year in reduced bug fixes with such tests. We offer a free assessment of your project — contact us to discuss testing setup.
Our experience
We have been developing Flutter applications for over 5 years. We have tested more than 20 projects with average 85% line coverage, from startups to enterprise solutions. We use the latest versions of Dart and packages, and follow official testing documentation. Our engineers are ready to train your team in test culture.
Get a free assessment of your project — we will analyze the current coverage and propose an improvement plan.







