Unit Test Development for Flutter Application
Flutter comes with flutter_test out of box, but writing good tests — not same as just writing tests. Typical Flutter project problem: tests exist, but test only "sunny day scenario," break on slightest structure change, or hold real HTTP requests inside.
Stack for Unit Tests
-
flutter_test— built-in, main -
mocktail— preferable tomockitofor Dart: no codegen required -
bloc_test— for BLoC/Cubit -
riverpod+ProviderContainer— for Riverpod-based logic -
fake_async— testing code withFuture.delayedandTimer
Testing BLoC
BLoC — most testable architecture in Flutter. bloc_test makes asserting state sequence trivial:
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 delay or async — await cubit.login(...) inside act.
Testing Riverpod
ProviderContainer allows creating 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');
});
Testing Use Case and Repository
Use Case — pure business logic without Flutter dependencies. Tests simply:
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
});
Common error: test Use Case via ViewModel/BLoC, not directly. Makes test fragile and slow.
fake_async for Code with Timers
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);
});
});
fakeAsync lets control time without actual sleep — tests with debounce/throttle run instantly.
Typical Errors
-
mocktailwithoutregisterFallbackValuefor custom types —any()doesn't work with non-standard classes without registration -
Tests that mutate global state —
SharedPreferencesorHivein tests must initialize viaSharedPreferences.setMockInitialValues({})before each test -
Missing
tearDown—ProviderContainer.dispose()andStreamController.close()forgotten, tests leak memory
CI Integration
flutter test --coverage → lcov.info → genhtml for HTML report. In GitHub Actions add step with flutter analyze + flutter test on each PR. For coverage filtering (exclude generated files) — remove_from_coverage package or sed filter on lcov.info.
Timeframe: 3–5 days depending on project volume and used architecture (BLoC / Riverpod / GetX).







