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

TypeTestsSpeedLives in
Unit testA single function or class, in isolation, no UI involvedFastesttest/
Widget testA single widget's UI and behavior, using an in-memory rendering (no full running app)Fasttest/
Integration testA complete app flow, on a real device or emulatorSlowest, most realisticintegration_test/
General guideline: write many unit tests (cheap, fast, isolate logic bugs precisely), a healthy number of widget tests for important UI, and a handful of integration tests for your most critical end-to-end flows.

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.

Next up: head to Building & Releasing to turn your tested app into a real build you can submit to the Play Store or App Store.