The testing gap is real and fixable
Teams ship AI chat features that look polished in demos and then discover production bugs that any ordinary feature would have caught: double-tap send buttons that spin forever, crashes after closing the app mid-stream, blank responses with no error UI, and test suites that still pass after error strings change because they asserted on the wrong thing.
None of those failures live in the model. They live in the Flutter code that wraps the model. The common belief that non-deterministic model output makes testing pointless is a category error. You are not testing Gemini. You are testing what your app does when any response arrives, when the stream errors, when safety filters block content, or when the rate limiter trips.
What you actually own
Your responsibility covers the repository that maps responses and exceptions into domain types, the Bloc that drives loading, streaming and error states, the widgets that render indicators and attribution labels, the rate limiter, the prompt sanitizer, and the system-prompt constraints. All of it is deterministic once you control the inputs. Mock the AI client; assert on your code.
A typical AI chat feature has at least six states: idle, connection establishment, streaming in progress, complete, various error subtypes, and content blocked. Each transition needs coverage. Streaming itself requires generators that yield chunks over time so accumulation and cancellation logic can be exercised.
Three-layer strategy
Unit tests form the base of the pyramid. They cover repository methods, Bloc state sequences, rate limiting, prompt sanitization and token logging. Tools are dart test, bloc_test and mocktail; the AI client is fully mocked. These tests run in milliseconds and catch the majority of logic bugs.
Widget tests sit in the middle. They verify chat-screen rendering, disabled send buttons during streaming, error banners, golden images of Markdown output, and that user taps dispatch the expected events. Fake Blocs or repositories keep the tests fast and deterministic.
Integration tests sit at the top and are deliberately few. They exercise the real Firebase Local Emulator stack, navigation lifecycle, offline behavior and stream cancellation without ever calling the live Gemini API. A stubbed function implementation keeps the tests hermetic.
Shared helpers live in one place: fake response builders that match the exact shape of the real client objects, stream controllers for progressive updates, and custom matchers. When the upstream response type changes, you update the fake once.
Core patterns that matter
Dependency injection is non-negotiable. The repository and rate limiter are passed into the Bloc constructor so tests can supply mocks. Fresh mocks are created in setUp so state never leaks between tests.
Repository tests assert that invalid input never reaches the model, that Firebase exceptions become domain exceptions, that truncated responses carry a truncation note, and that token usage is logged. Widget tests confirm the send button disables while streaming, re-enables after error, and that the flag button and attribution label required by store policy are present.
Bloc tests use bloc_test to declare the exact sequence of states for a multi-chunk stream, for an immediate stream error, and for a rate-limit rejection. Golden tests lock the visual appearance of plain text, Markdown, streaming and flagged states so layout regressions surface in CI.
Prompt-sanitizer tests target known injection patterns one by one and also verify that legitimate questions still pass. A simple word-count assertion on the system-prompt string itself forces conscious decisions when instructions grow.
What the suite still cannot catch
Model quality regressions after a Gemini update, the real-world effectiveness of a system prompt against live adversarial inputs, and novel injection techniques not yet encoded in the sanitizer all sit outside automated tests. Those require human evaluation and ongoing security monitoring. Everything else that is your code is fully testable with the patterns above.
The discipline is to write the tests before launch, not after the first production incident. The happy path is roughly ten percent of real user behavior. The other ninety percent is what most AI feature suites still leave uncovered.