How to test AI features in Flutter the right way

Testing AI features in Flutter means testing your repository, Bloc, widgets and error handlers, not the model. A full handbook with unit, widget and integration patterns.

MiHiR SEN
MiHiR SEN
·4 min read
This handbook explains how to test Flutter AI features by focusing on application code rather than the model. It covers a three-layer strategy of unit, widget and integration tests, with concrete patterns for mocking Gemini responses, exercising streaming states, verifying rate limits, prompt sanitization, golden images and compliance elements such as attribution labels and flag buttons. The result is a reusable test foundation that catches the deterministic bugs that actually ship in AI chat UIs.

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.