Testing
Automated tests catch regressions before your users do — and Flutter's tooling makes writing them genuinely approachable, whether you're testing a single function or an entire user flow. Flutter supports three distinct levels of testing, each trading off speed against realism, and this lesson covers all three plus how to mock external dependencies so tests stay fast and reliable.
The Three Test Types
| Type | Tests | Speed | Lives in |
|---|---|---|---|
| Unit test | A single function or class, in isolation, no UI involved | Fastest | test/ |
| Widget test | A single widget's UI and behavior, using an in-memory rendering (no full running app) | Fast | test/ |
| Integration test | A complete app flow, on a real device or emulator | Slowest, most realistic | integration_test/ |
Writing a Unit Test With the test Package
A unit test checks a single piece of logic — a function, a method on a class — completely
independent of any UI. Dart's test package (installed automatically with any Flutter
project) provides test() to define a test case and expect() to assert
an outcome.
Example: Testing a Plain Dart Function
int add(int a, int b) => a + b;
void main() {
test('adds two numbers', () {
expect(add(2, 3), 5);
});
test('adds negative numbers correctly', () {
expect(add(-4, 10), 6);
});
}
No widget, no rendering, no device needed — this test runs in plain Dart in milliseconds, which is exactly why unit tests should cover as much of your business logic as reasonably possible.
Writing a Widget Test With testWidgets()
A widget test checks that a widget renders correctly and responds to interaction as expected,
using flutter_test's testWidgets() function and a WidgetTester
that simulates a real widget tree without launching an actual app on a device.
Example: Testing a Counter Button Increments the Displayed Count
void main() {
testWidgets('tapping the + button increments the counter', (WidgetTester tester) async {
// Build the widget and trigger a frame
await tester.pumpWidget(const MaterialApp(home: CounterScreen()));
// Verify the initial count is 0
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon button and rebuild the widget tree
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify the count updated to 1
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
tester.pumpWidget() builds the widget tree, find.text() and
find.byType()/find.byIcon() locate specific widgets within it,
tester.tap() simulates a real user tap, and tester.pump() triggers a
rebuild so setState() changes actually take effect before the next assertion — a good
companion to what you already know about setState() from
State Management: setState.
Running Your Tests
Both unit tests and widget tests run with a single command from your project root:
flutter test
This runs every test file under test/ and prints a pass/fail summary — genuinely fast
enough to run constantly while developing, and exactly the kind of check that belongs in a CI
pipeline before merging changes.
Mocking Dependencies With mocktail or mockito
Tests should be fast and deterministic — which means they shouldn't make real network calls, hit a
real database, or depend on external services being up. When code depends on something like an
HTTP client (as seen in HTTP Requests & APIs), packages like
mocktail or mockito let you create a fake version of that dependency that
returns controlled, predictable responses instead.
Example: Faking an HTTP Client So a Test Never Hits the Network
class MockHttpClient extends Mock implements http.Client {}
void main() {
test('parses a successful response into a User', () async {
final client = MockHttpClient();
when(() => client.get(Uri.parse('https://api.example.com/user/1')))
.thenAnswer((_) async => http.Response('{"id":1,"name":"Sonu"}', 200));
final user = await fetchUser(client, 1);
expect(user.name, 'Sonu');
});
}
Because fetchUser receives the client as a parameter rather than creating its own, the
test can swap in a mock that never actually touches the network, while still exercising the real
parsing logic — the JSON decoding covered in
JSON Serialization.