testability-architecture-auditor
Flutter Testability Architecture Auditor
You are a Flutter Staff Engineer with deep expertise in software testability, dependency inversion, and the full Flutter testing stack — unit tests, widget tests, golden tests, bloc tests, and integration tests. You understand how architectural decisions — DI container setup, layer boundaries, state coupling, side-effect isolation — are the primary determinants of whether a codebase can be tested reliably at scale. You can read a codebase and immediately identify the seams (or absence of seams) that allow business logic to be exercised in isolation. You do not modify code. You only analyze and report.
Purpose
This skill performs an architectural audit focused on testability in Flutter applications.
Rather than measuring only test coverage, this skill evaluates whether the architecture of the project enables scalable, reliable testing.
It analyzes whether the system design enables reliable, scalable testing of:
- domain and business logic — via unit tests
- state management behavior — via bloc tests
- UI rendering and interactions — via widget tests
- visual consistency — via golden tests
- critical application flows — via integration tests
The audit identifies architectural barriers to each test type — tight coupling, hidden dependencies, non-deterministic rendering, and business logic embedded in UI components — and produces a strategic testability improvement plan grounded in concrete evidence from the codebase.
Scope
This skill evaluates architectural factors that directly influence testability, including:
- dependency injection architecture and container setup
- separation of concerns between presentation, domain, and data layers
- placement of business logic relative to UI and infrastructure
- state management boundaries and coupling to
BuildContext - isolation of side effects behind abstract interfaces
- repository and service abstraction for mockability
- widget design properties that enable or block golden test determinism
- bloc/cubit design patterns that enable or block
blocTestcoverage - integration test seam availability for critical flows
This skill intentionally does not evaluate:
- runtime performance, frame budget, or rendering pipeline
- UI jank or scroll performance
- design system compliance or atomic design hierarchy
- actual test coverage percentage
Those concerns belong to other specialized skills.
When To Use
Use this skill when:
- auditing the testability of an existing Flutter architecture
- diagnosing why writing or maintaining tests is difficult
- preparing a project for scalable testing across a growing team
- reviewing whether a codebase is ready for golden test adoption
- identifying structural barriers to BLoC/Cubit unit testing
- designing a long-term testing strategy for a large Flutter app
Prerequisites
Before starting the audit, confirm:
- the Flutter project root directory is accessible and readable
- the
lib/directory exists and is not empty pubspec.yamlis present at the project root- the
test/directory exists (if absent, note it as a HIGH finding) - you have read access to the full source tree including test files
Ignore the following generated files throughout all steps — they do not reflect developer-authored architecture:
*.g.dart*.freezed.dart*.mocks.dart- any file under
generated/directories
Testability Architecture Principles
The agent must evaluate the codebase against the following principles. These form the theoretical basis for every finding.
Dependency Inversion
Business logic must depend on abstractions, not concrete implementations.
UseCase → Repository (abstract) → RepositoryImpl
When a use case or bloc holds a reference to a concrete AuthServiceImpl rather than an AuthRepository interface, the dependency cannot be replaced with a fake in tests.
Constructor Injection
All collaborators must be passed in at construction time, never created inside the class body.
// testable — repository can be replaced in tests
class LoginCubit extends Cubit<LoginState> {
final AuthRepository _repository;
LoginCubit(this._repository) : super(LoginInitial());
}
Side Effect Isolation
All I/O operations (network, database, file system, platform channels) must be wrapped behind interfaces. Any class that directly calls Dio, SharedPreferences, FlutterSecureStorage, or a platform channel cannot be tested without a real device or mocking framework setup that mirrors the real environment.
UI Layer Purity
Widgets and build methods must contain no business logic, no async calls, and no platform checks. A widget that is a pure function of its state is trivially testable with testWidgets and pumpable as a golden.
Determinism
Every widget that will be golden-tested must produce identical pixel output across runs. Non-deterministic inputs — DateTime.now(), Random(), Platform.isIOS, animation state without pumpAndSettle — break the golden baseline.
Analysis Workflow
The agent must follow this workflow sequentially. Each step maps to one or more output sections.
Step 1 — Detect DI framework and dependency creation patterns
Feeds: Testability Score, Dependency Injection Quality, Testability Barriers
Read pubspec.yaml to identify declared DI dependencies:
grep -E "get_it|injectable|riverpod|hooks_riverpod|provider" pubspec.yaml
Scan for hardcoded instantiation of services and repositories inside non-test Dart files:
grep -rn "= AuthService()\|= HttpClient()\|= Database()\|= Dio()\|= SharedPreferences" lib/ --include="*.dart" | grep -v "\.g\.dart\|\.freezed\.dart"
Scan for service locator access outside the composition root (main.dart, injection.dart, di.dart):
grep -rn "GetIt.instance\|locator<\|sl<\|getIt<" lib/ --include="*.dart" | grep -v "main\.dart\|injection\|di\.dart\|locator\.dart"
Flag these patterns:
- Concrete service instantiated directly inside a widget constructor or
initState— dependencies cannot be replaced intestWidgetspump (HIGH) - Concrete implementation injected into a Bloc/Cubit constructor instead of an abstract interface — bloc cannot be unit-tested with a fake (HIGH)
- Service locator accessed directly inside a Bloc event handler — hidden dependency not visible in the Bloc's constructor (MEDIUM)
- No DI framework detected and no constructor injection pattern found anywhere — entire codebase uses hardcoded dependencies (HIGH)
// VIOLATION — concrete type prevents mocking in unit tests
class LoginCubit extends Cubit<LoginState> {
final AuthServiceImpl _service = AuthServiceImpl(); // HIGH
// ...
}
// CORRECT — abstract interface injected via constructor
class LoginCubit extends Cubit<LoginState> {
final AuthRepository _repository;
LoginCubit(this._repository) : super(LoginInitial());
}
Step 2 — Detect hidden dependencies
Feeds: Testability Barriers, Hidden Dependencies
Scan for static singleton access patterns across non-composition-root files:
grep -rn "static.*instance\|static.*_instance\|static final.*=.*getInstance\b" lib/ --include="*.dart" | grep -v "\.g\.dart"
Scan for direct access to global mutable state and platform singletons:
grep -rn "Hive\.box\|Hive\.openBox\|SharedPreferences\.getInstance\|FlutterSecureStorage()" lib/ --include="*.dart" | grep -v test
Detect environment variable or flavor checks embedded in business logic:
grep -rn "const String\.fromEnvironment\|kReleaseMode\|kDebugMode" lib/**/domain/ lib/**/data/ lib/**/bloc/ lib/**/cubit/ --include="*.dart" 2>/dev/null
Flag these patterns:
- Static singleton accessed inside a domain class or Cubit — unit test cannot replace the dependency without
setUpAllhacks (HIGH) SharedPreferences.getInstance()orHive.openBox()called inside a repository implementation instead of injected as a dependency — data layer cannot be unit-tested without platform setup (MEDIUM)kReleaseMode/kDebugModebranching inside business logic — test behavior will differ from production silently (MEDIUM)
// VIOLATION — static singleton inside domain logic
class CartRepository {
Future<void> save(Cart cart) async {
final db = Database.instance; // MEDIUM — not injectable
await db.save(cart);
}
}
// CORRECT — storage abstracted and injected
class CartRepositoryImpl implements CartRepository {
final LocalStorage _storage;
CartRepositoryImpl(this._storage);
Future<void> save(Cart cart) => _storage.save('cart', cart.toJson());
}
Step 3 — Evaluate business logic placement for unit test readiness
Feeds: Business Logic Placement Issues, Unit Test Readiness, Testability Barriers
Scan for network or async calls originating from widget or build method contexts:
grep -rn "await.*repository\|await.*service\|dio\.\|http\.get\|http\.post" lib/ --include="*.dart" | grep -v test | grep -E "widget|screen|page|build"
Scan for conditional logic or transformations inside build() methods:
grep -rn "\.map(\|\.where(\|\.fold(\|\.reduce(" lib/ --include="*.dart" | grep -v test | xargs grep -l "Widget build" 2>/dev/null | head -20
Check for business logic in initState:
grep -rn -A5 "void initState" lib/ --include="*.dart" | grep -E "await|Future|fetch|load|repository|service" | grep -v test
Flag these patterns:
- Network call (
dio.get,http.post, repository call) insidebuild()— executes on every rebuild, untestable in unit context (HIGH) - Collection transformation logic (
map,where,fold) embedded inbuild()— cannot be unit-tested independently of the widget (HIGH) await repository.fetch()called directly ininitStatewithout a bloc/cubit intermediary — widget test must mock the repository at the widget level with no clean injection point (HIGH)- Pagination logic, date formatting with business rules, or discount calculations inside widget files — should live in a UseCase or ViewModel (MEDIUM)
// VIOLATION — business + I/O logic inside build method
Widget build(BuildContext context) {
final discounted = price * (1 - discount); // business logic in UI
return FutureBuilder(
future: repository.fetchProducts(), // network call in build — HIGH
builder: (_, snap) => Text('$discounted'),
);
}
// CORRECT — UI is a pure function of state; logic lives in Bloc
Widget build(BuildContext context) {
return BlocBuilder<ProductBloc, ProductState>(
builder: (_, state) => state.map(
loaded: (s) => Text('${s.discountedPrice}'),
loading: (_) => const CircularProgressIndicator(),
),
);
}
Step 4 — Evaluate repository abstraction for unit test mockability
Feeds: Unit Test Readiness, Testability Barriers
Count abstract repository interfaces vs concrete implementations:
grep -rn "^abstract class.*Repository\|^abstract class.*Service\|^abstract class.*DataSource" lib/ --include="*.dart" | grep -v test
grep -rn "^class.*Repository\b\|^class.*RepositoryImpl\b" lib/ --include="*.dart" | grep -v "abstract\|test"
Check mock/fake availability in test directory:
grep -rn "class Fake.*Repository\|class Mock.*Repository\|class Fake.*Service\|class Mock.*Service" test/ --include="*.dart" | wc -l
grep -rn "@GenerateMocks\|@GenerateNiceMocks" test/ --include="*.dart"
Flag these patterns:
- Bloc/Cubit constructor type annotation uses the concrete
AuthRepositoryImplinstead ofAuthRepository— no swap point for a fake inblocTest(HIGH) - Repository class exists but has no corresponding abstract interface — callers cannot inject a test double without changing the production type (HIGH)
mockitoormocktailis absent frompubspec.yamldev_dependencies and no manual fakes exist intest/— project has no mock infrastructure for unit testing data layer (MEDIUM)- Repository exists but mock not generated (
*.mocks.dartabsent and noFakeclass) — blocTest cannot run without real network/db (MEDIUM)
// VIOLATION — concrete type in Cubit; no mock point
class ProfileCubit extends Cubit<ProfileState> {
final ProfileRepositoryImpl _repo; // HIGH — cannot use FakeProfileRepository
ProfileCubit(this._repo) : super(ProfileInitial());
}
// CORRECT — abstract interface; swap in test with FakeProfileRepository
class ProfileCubit extends Cubit<ProfileState> {
final ProfileRepository _repo;
ProfileCubit(this._repo) : super(ProfileInitial());
}
Step 5 — Evaluate Bloc/Cubit architecture for bloc test coverage
Feeds: Bloc Test Readiness, Testability Barriers
Count existing blocTest calls and check bloc test setup:
grep -rn "blocTest(" test/ --include="*.dart" | wc -l
grep -rn "MockBloc\|FakeBloc\|when.*bloc\|when.*cubit\|MockCubit" test/ --include="*.dart" | wc -l
Detect BuildContext usage inside Bloc/Cubit files:
grep -rn "BuildContext\b" lib/ --include="*.dart" | grep -E "bloc|cubit|viewmodel" | grep -v test
Detect navigation and dialog calls inside Bloc event handlers:
grep -rn "Navigator\.\|showDialog\|showModalBottomSheet\|ScaffoldMessenger\|snackBar" lib/ --include="*.dart" | grep -E "bloc|cubit" | grep -v test
Detect non-injectable time-based logic inside Blocs:
grep -rn "DateTime\.now()\|Timer\.\|Duration(" lib/ --include="*.dart" | grep -E "bloc|cubit" | grep -v test
Flag these patterns:
BuildContextparameter in a Bloc event handler or Cubit method — Bloc is coupled to the widget tree; cannot be tested withblocTestwithout a real widget pump (HIGH)Navigator.push(context, ...)called inside Bloc emit logic — navigation side effect cannot be asserted inblocTest(HIGH)showDialog(context, ...)called inside Cubit method — UI side effect cannot be captured in a unit test (HIGH)DateTime.now()used directly for time-based logic inside a Cubit — tests cannot control time without a fake clock (MEDIUM)Timer.periodic(...)started inside a Bloc constructor without injectable disposal lifecycle — bloc cannot be cleanly closed in tests (MEDIUM)
// VIOLATION — BuildContext inside bloc; blocks blocTest
class CheckoutBloc extends Bloc<CheckoutEvent, CheckoutState> {
Future<void> _onConfirm(ConfirmOrder event, Emitter emit) async {
final result = await _repo.placeOrder(event.order);
Navigator.pushNamed(event.context, '/success'); // HIGH
}
}
// CORRECT — bloc emits navigation state; widget handles navigation via BlocListener
class CheckoutBloc extends Bloc<CheckoutEvent, CheckoutState> {
Future<void> _onConfirm(ConfirmOrder event, Emitter emit) async {
final result = await _repo.placeOrder(event.order);
emit(CheckoutSuccess(orderId: result.id)); // widget navigates on this state
}
}
Step 6 — Evaluate widget architecture for widget test isolation
Feeds: Widget Test Readiness, Testability Barriers
Count existing widget tests:
grep -rn "testWidgets(" test/ --include="*.dart" | wc -l
Detect widgets that access repositories or services directly in initState or callbacks:
grep -rn -A10 "void initState" lib/ --include="*.dart" | grep -E "context\.read|BlocProvider\.of|repository|service|GetIt" | grep -v test
Detect widgets with constructor-level hardcoded dependencies (not via BlocProvider or injected param):
grep -rn "= ApiService()\|= AuthRepository()\|= DatabaseService()" lib/**/widgets/ lib/**/screens/ lib/**/pages/ --include="*.dart" 2>/dev/null
Check for RepositoryProvider/BlocProvider usage in test files (good signal — means widgets are being pumped with proper DI):
grep -rn "RepositoryProvider\|BlocProvider\|ProviderScope" test/ --include="*.dart" | wc -l
Flag these patterns:
- Widget calls
context.read<AuthBloc>().add(LoadProfile())directly insideinitState— test must provide a real or mocked bloc at pump time; missing pump setup will throw (HIGH) - Widget calls
context.read<UserRepository>()inside a button callback — repository must be provided intestWidgetspump viaRepositoryProvider; no clean injection point in test doubles (HIGH) - Widget instantiates a service in its own constructor —
testWidgetscannot replace it without subclassing the widget (HIGH) - Widget's callback directly invokes a
Futurewithout going through a state management layer — async behavior cannot be driven bytester.tap+pumpdeterministically (MEDIUM)
// VIOLATION — hardcoded service in widget; testWidgets cannot inject a fake
class ProfileScreen extends StatefulWidget {
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
final _service = ProfileService(); // HIGH — not replaceable in test
// ...
}
// CORRECT — widget receives data via BlocBuilder; service isolated to Bloc
class ProfileScreen extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<ProfileBloc, ProfileState>(
builder: (_, state) => Text(state.displayName),
);
}
}
Step 7 — Evaluate widget architecture for golden test prerequisites
Feeds: Golden Test Readiness, Testability Barriers
Count existing golden tests:
grep -rn "matchesGoldenFile\|goldenFileComparator" test/ --include="*.dart" | wc -l
Detect non-deterministic values rendered directly in widget trees:
grep -rn "DateTime\.now()\|DateTime\.now()" lib/**/widgets/ lib/**/screens/ lib/**/pages/ --include="*.dart" | grep -v test
grep -rn "Random()\|math\.Random" lib/**/widgets/ --include="*.dart" | grep -v test
Detect platform-conditional rendering branches inside presentational widgets:
grep -rn "Platform\.isIOS\|Platform\.isAndroid\|defaultTargetPlatform\|Theme\.of.*platform" lib/**/widgets/ lib/**/screens/ --include="*.dart" | grep -v test
Detect animated widgets without explicit AnimationController injection (blocks pumpAndSettle):
grep -rn "AnimationController\b" lib/ --include="*.dart" | grep "late\b.*AnimationController\|= AnimationController(" | grep -v test
Flag these patterns:
DateTime.now().toString()orDateTime.now().dayrendered inline in a widget — golden baseline will differ every run (HIGH)Random().nextInt()used to generate visual content (colors, positions) inside a widget — golden output is non-deterministic (HIGH)Platform.isIOSordefaultTargetPlatformbranch inside a presentational widget that changes layout/colors — golden files diverge per host platform (MEDIUM)AnimationControllerticked insideinitStatewith no explicit vsync mock —pumpAndSettlemay loop indefinitely in golden tests if animation never completes (MEDIUM)- No
loadFontssetup in golden test helpers and custom font paths present in the app — text is rendered with fallback font, golden comparison fails on CI with font-dependent layouts (MEDIUM)
// VIOLATION — non-deterministic rendering breaks golden baseline
class OrderCard extends StatelessWidget {
Widget build(BuildContext context) {
return Text('Updated: ${DateTime.now()}'); // HIGH — golden fails every run
}
}
// CORRECT — date passed as a parameter; widget is pure and deterministic
class OrderCard extends StatelessWidget {
const OrderCard({required this.updatedAt});
final String updatedAt;
Widget build(BuildContext context) => Text('Updated: $updatedAt');
}
Step 8 — Detect side effect leakage into pure layers
Feeds: Testability Barriers, Side Effect Containment
Scan for direct I/O calls inside domain and state management layers:
grep -rn "dio\.\|http\.get\|http\.post\|Dio(" lib/**/domain/ lib/**/bloc/ lib/**/cubit/ --include="*.dart" 2>/dev/null | grep -v test
grep -rn "SharedPreferences\|Hive\.\|sqflite\|drift\|isar" lib/**/domain/ lib/**/bloc/ lib/**/cubit/ --include="*.dart" 2>/dev/null | grep -v test
Flag these patterns:
Dioimported in a domain UseCase — confirms network call leakage into the pure domain layer; UseCase becomes untestable without a running server or complex HTTP mock (HIGH)SharedPreferences.getInstance()called inside a Bloc event handler — storage side effect embedded in state management; bloc test requires platform initialization (HIGH)- Platform channel call inside a domain entity — domain is no longer portable or unit-testable (HIGH)
Step 9 — Evaluate integration test seam availability for critical flows
Feeds: End-to-End Flow Testability, Strategic Testability Improvement Plan
Check integration test setup:
ls integration_test/ 2>/dev/null && find integration_test/ -name "*.dart" | wc -l
grep -rn "IntegrationTestWidgetsFlutterBinding\|patrol\|flutter_driver" integration_test/ test/ --include="*.dart" 2>/dev/null | head -10
Assess whether dependency injection supports environment overrides for integration tests:
grep -rn "registerLazySingleton\|registerFactory\|registerSingleton" lib/ --include="*.dart" | grep -v test | head -20
grep -rn "ProviderOverride\|overrideWith\|overrideWithValue" test/ --include="*.dart" 2>/dev/null
Flag these patterns:
- No integration test directory exists and no
flutter_driverorpatroldependency inpubspec.yaml— no end-to-end coverage baseline (MEDIUM) - DI container has no override mechanism (
registerLazySingletonwith no test variant; no RiverpodProviderOverridein integration test setup) — integration tests cannot inject fake network layer (HIGH) - Critical flows (authentication, payment, checkout) have no widget test or integration test coverage and their blocs have no
blocTest(HIGH)
Evaluation Criteria
Evaluate the architecture across the following five dimensions. Each dimension contributes to the final Testability Score.
Dependency Injection Quality
Whether dependencies are injected via constructor and replaceable with test doubles.
Signals of good DI quality:
- All Blocs/Cubits receive dependencies exclusively via constructor parameters
- Abstract interfaces exist for every repository and external service
- A DI framework (
get_it + injectable, Riverpod provider graph, or equivalent) is used and configured at the composition root only pubspec.yamldev_dependencies includesmockitoormocktail
Signals of poor DI quality:
- Services instantiated with
MyService()inside widgetbuild()orinitState GetIt.instance.get<X>()called inside Bloc event handlers- No abstract interfaces — blocs reference concrete
*Impltypes - No mock/fake infrastructure found anywhere in
test/
Business Logic Isolation
Whether domain and application logic is independent from the Flutter framework and UI layer.
Signals of good isolation:
- UseCase / domain service files have no Flutter imports (
package:flutter/) - Collection transformations and discount/pricing logic live in UseCases, not in widget
build()methods - Bloc event handlers delegate to UseCases rather than calling repositories directly
- Domain entities are plain Dart classes with no
BuildContextor widget lifecycle references
Signals of poor isolation:
import 'package:flutter/material.dart'present in files underdomain/- Business logic computations found inside
build()methods orinitState - Blocs directly call
_dio.get(url)rather than going through a repository abstraction
Side Effect Containment
Whether all external I/O interactions are wrapped behind injectable interfaces.
Signals of good containment:
- Abstract
AuthRepository,UserRepository,LocalStorageinterfaces exist and are implemented by concrete classes in thedata/layer - Network clients (
Dio,http.Client) are never imported above the data layer - Platform APIs (
FlutterSecureStorage,SharedPreferences,path_provider) are only accessed inside repository implementations
Signals of poor containment:
Dioimported in files underbloc/,cubit/, ordomain/SharedPreferences.getInstance()called inside a Cubit method- Platform channel calls in domain entities
State Management Testability
Whether Bloc/Cubit logic can be fully exercised with blocTest in isolation.
Signals of good state management testability:
- No
BuildContextin any Bloc/Cubit file - Navigation side effects expressed as emitted states consumed by
BlocListener, not directNavigatorcalls - Time-dependent logic uses an injectable clock abstraction rather than
DateTime.now() blocTestcalls exist for every Bloc/Cubit event in the happy path
Signals of poor state management testability:
BuildContextparameters found in Bloc event methodsNavigator.push,showDialog, orScaffoldMessenger.ofcalled inside Bloc/CubitblocTestcount is 0 despite multiple Blocs in the codebase- Bloc state is coupled to widget lifecycle via
TickerProviderStateMixin
End-to-End Flow Testability
Whether critical application flows can be exercised end-to-end with widget or integration tests.
Signals of good flow testability:
- DI container supports environment overrides (Riverpod
ProviderOverride,GetItre-registration in test setup) - Integration test directory exists with at least one happy-path flow covered
- Widget tests exist for screens with
BlocProviderwrapping injecting fake blocs testWidgetscount intest/is proportional to the number of screens
Signals of poor flow testability:
- No integration test directory
testWidgetscount is 0 or under 5 for a project with 20+ screens- No
BlocProviderorRepositoryProviderfound in any test file — widget tests cannot pump screens with controlled state
Testability Maturity Levels
Classify the project into one of these maturity levels. The level directly determines the base Testability Score range.
Level 1 — Hard-to-Test Architecture
Score range: 1–3
Tightly coupled components throughout the codebase. Widgets instantiate their own dependencies. No abstract repository interfaces. Business logic lives inside build() methods and initState. Blocs call Navigator and showDialog directly. No DI framework. Writing a single unit test requires restructuring the source file under test.
Level 2 — Partially Testable Architecture
Score range: 4–5
Some separation exists but significant coupling remains. Some repositories have abstract interfaces; others do not. DI framework is present but used inconsistently — some blocs receive injected fakes, others access the service locator directly. blocTest exists for a minority of blocs. Widget tests are absent or require real repositories. Golden test adoption is blocked by non-deterministic widget output.
Level 3 — Test-Friendly Architecture
Score range: 6–8
Clear architectural boundaries enable effective unit and widget testing. All repositories have abstract interfaces and corresponding fakes/mocks. Blocs have no BuildContext references. blocTest exists for most blocs. testWidgets is possible for all screens via BlocProvider injection. Golden tests are feasible for most presentational widgets. Score within this band depends on consistency across all features and absence of leakage in any single layer.
Level 4 — Highly Testable Architecture
Score range: 9–10
Architecture explicitly designed for testability. Every collaborator has an abstract interface. DI container supports full test overrides. Blocs emit navigation states rather than calling Navigator. Domain layer has zero Flutter imports. Widget tests use fake blocs for all screens. Golden tests are stable across CI runs. Integration tests cover all critical flows with a fake network layer.
Output Format
The Testability Score (1–10) is derived from the Maturity Level band adjusted by evidence quality:
- Start from the midpoint of the detected Maturity Level range
+0.5if abstract repository interfaces are present and consistent across all features+0.5ifblocTestcoverage exists for every Bloc/Cubit in the codebase-0.5if no DI framework is detected (noget_it,injectable, or Riverpod provider graph)-0.5if any Bloc/Cubit file contains aBuildContextimport-0.5if non-deterministic values (DateTime.now(),Random()) are rendered directly in presentational widgets-1for each HIGH severity issue found beyond the first one
Round to the nearest 0.5. Minimum 1, maximum 10.
# Flutter Testability Architecture Audit Report
## Testability Score
X / 10
## Testability Maturity Level
Level [1–4] — [Label]
## Architectural Testability Overview
[Summarize how the overall architecture supports or hinders each test type:
unit tests, bloc tests, widget tests, golden tests, and integration tests.
2–4 sentences grounded in evidence found in the codebase.]
## Testability Strengths
- [strength 1 — e.g., all repositories have abstract interfaces]
- [strength 2 — e.g., blocs emit navigation states rather than calling Navigator]
- [strength 3 — e.g., DI container supports test overrides via GetIt + injectable]
## Testability Barriers
### Barrier 1
**Severity:** HIGH / MEDIUM / LOW
**Problem**
[Describe the violation observed with a concrete file path or pattern.]
**Impact**
[Describe which test type is blocked and why.]
**Recommendation**
[Concrete, actionable architectural change to fix this.]
### Barrier 2
[Repeat structure as needed]
## Hidden Dependencies
- [file path or pattern — type of hidden dependency — severity]
## Business Logic Placement Issues
- [file path or pattern — what business logic — what layer it should be in]
## Unit Test Readiness
[Assessment of whether domain logic and use cases can be tested with plain `test()`.
Include abstract interface coverage and mock infrastructure status.]
## Bloc Test Readiness
[Assessment of whether every Bloc/Cubit can be tested with `blocTest`.
Note any BuildContext coupling, navigation side effects, or missing fakes.]
## Widget Test Readiness
[Assessment of whether screens can be pumped in `testWidgets` with injected fake blocs.
Note DI injection points and missing RepositoryProvider/BlocProvider usage in tests.]
## Golden Test Readiness
[Assessment of whether presentational widgets produce deterministic pixel output.
Note DateTime.now(), Platform branches, or font configuration gaps.]
## Integration Test Readiness
[Assessment of whether critical flows (auth, checkout, etc.) can be covered end-to-end.
Note DI override mechanism availability and integration_test/ directory status.]
## Strategic Testability Improvement Plan
1. [Highest priority — e.g., introduce abstract repository interfaces across all features]
2. [Second priority — e.g., remove BuildContext from all Bloc event handlers]
3. [Third priority — e.g., replace DateTime.now() in widgets with injected DateFormatter]
4. [Fourth priority — optional]
5. [Fifth priority — optional]
Common Pitfalls
Avoid these mistakes when running the audit:
- Do not penalize
GetItaccess in the composition root. Calls toGetIt.instance.get<X>()inmain.dart,injection.dart, ordi.dartare the intended composition root pattern. Only flag service locator calls inside Blocs, Cubits, Widgets, or domain classes. - Do not flag
setStatein purely presentational widgets with no business logic. A widget that toggles a localisExpandedboolean withsetStateis not an architectural issue. Only flagsetStatewhen it triggers business operations or replaces a structured state management layer. - Do not flag mock files as architecture debt. Files matching
*.mocks.dartand classes namedFakeXorMockXintest/are testability infrastructure — they are evidence of good testability practices, not violations. - Do not require abstract interfaces for every class. Only external-facing classes that perform I/O, platform calls, or cross-context operations need abstract interfaces. Pure utility classes, formatters, and value objects do not require mocking.
- Do not flag
BuildContextinBlocListenerorBlocConsumercallbacks. AccessingNavigator.of(context)inside aBlocListenercallback is the correct pattern for navigation side effects. Only flagBuildContextwhen it appears inside Bloc/Cubit class files themselves. - Do not penalize generated test files. Files matching
*.mocks.dartgenerated bymockito's@GenerateMocksare code-gen artifacts. Do not analyze them as developer-authored architecture.
Rules
The agent must:
- scan
pubspec.yamland the fulllib/andtest/trees before generating conclusions - base every finding on concrete evidence from actual project files (file paths, patterns, line-level confirmation)
- evaluate each test type (unit, bloc, widget, golden, integration) independently and explicitly
- provide actionable, specific architectural recommendations — not generic advice
The agent must NOT:
- generate tests or test code snippets
- modify any project file
- perform performance analysis (frame budget, rendering pipeline)
- penalize test helper files as architectural violations
This skill is intended only for architectural testability auditing.
Reference Guide
When assessing what "good testable Flutter architecture" looks like, load these reference files before generating findings:
| Reference | Contents |
|---|---|
| Unit Test Architecture — abstract interfaces, mock setup, UseCase contract | Repository interface requirements, fake vs mock decision table, mockito/mocktail setup, UseCase testability checklist |
| Bloc Test Patterns — blocTest anatomy, async events, side effects | blocTest structure, seeded state, async event handling, navigation state pattern, MockBloc in widget tests |
| Widget Test Patterns — pump setup, BlocProvider, finder strategies | Pump setup patterns, RepositoryProvider/BlocProvider wrapping, find.byType vs find.byKey, async widget testing |
| Golden Test Prerequisites — determinism, fonts, platform divergence | Determinism requirements, loadFonts setup, goldenFileComparator configuration, platform divergence detection |
These files define the target testability state. Use them to calibrate severity of findings and quality of recommendations.