Skip to content
Blog

Testing Mobile Apps: Unit, Widget, and Integration Tests

Master mobile app testing in Flutter with unit tests, widget tests, and integration tests. Build reliable apps with comprehensive test coverage strategies.

Published on August 13, 2026

AI Assistant

Mobile apps are unforgiving. Users uninstall buggy apps within seconds, and a single crash can tank your ratings. Yet many teams ship code with minimal testing, relying on manual QA and hope.

Flutter provides a built-in testing framework that covers every layer of your application. This guide walks through unit, widget, and integration tests with practical code examples you can adapt immediately.

Prerequisites

  • Flutter SDK 3.0+
  • Basic Dart knowledge
  • A Flutter project (run flutter create my_app if needed)
  • IDE with Flutter support

Why Testing Matters

Testing isn’t optional overhead—it’s a development accelerant. Well-written tests:

  • Catch regressions before users do
  • Enable confident refactoring
  • Document expected behavior
  • Reduce debugging time
  • Speed up code reviews

Flutter’s test pyramid follows the classic approach: many fast unit tests at the base, fewer widget tests in the middle, and a handful of integration tests at the top.

Unit Testing Business Logic

Unit tests verify individual functions, methods, and classes in isolation. They’re fast, deterministic, and form your safety net.

Setting Up

Add flutter_test to your pubspec.yaml:

dev_dependencies:
  flutter_test:
    sdk: flutter

Create your first test file at test/counter_test.dart:

import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/counter.dart';

void main() {
  group('Counter', () {
    test('starts at zero', () {
      final counter = Counter();
      expect(counter.value, 0);
    });

    test('increments by one', () {
      final counter = Counter();
      counter.increment();
      expect(counter.value, 1);
    });

    test('decrements by one', () {
      final counter = Counter();
      counter.decrement();
      expect(counter.value, -1);
    });

    test('cannot go below zero', () {
      final counter = Counter();
      counter.decrement();
      counter.decrement();
      expect(counter.value, -1);
    });
  });
}

Run tests with:

flutter test

Testing Async Code

Modern apps are async-heavy. Here’s testing a repository with network calls:

class UserRepository {
  final ApiClient _api;

  UserRepository(this._api);

  Future<User> getUser(String id) async {
    final response = await _api.get('/users/$id');
    return User.fromJson(response);
  }
}

// test/user_repository_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:my_app/user_repository.dart';

import 'user_repository_test.mocks.dart';

@GenerateMocks([ApiClient])
void main() {
  late MockApiClient mockApi;
  late UserRepository repository;

  setUp(() {
    mockApi = MockApiClient();
    repository = UserRepository(mockApi);
  });

  group('getUser', () {
    test('returns user on successful response', () async {
      when(mockApi.get(any)).thenAnswer(
        (_) async => {'id': '1', 'name': 'Test User'},
      );

      final user = await repository.getUser('1');

      expect(user.name, 'Test User');
      verify(mockApi.get('/users/1')).called(1);
    });

    test('throws on network error', () async {
      when(mockApi.get(any)).thenThrow(NetworkException());

      expect(
        () => repository.getUser('1'),
        throwsA(isA<NetworkException>()),
      );
    });
  });
}

Generate mocks with:

dart run build_runner build

Testing State Management

For state management (Provider, Riverpod, Bloc), test state transitions:

class CartBloc {
  final CartRepository _repo;
  List<CartItem> _items = [];

  CartBloc(this._repo);

  List<CartItem> get items => List.unmodifiable(_items);

  Future<void> addItem(Product product) async {
    _items.add(CartItem(product: product, quantity: 1));
    await _repo.save(_items);
  }

  Future<void> removeItem(String productId) async {
    _items.removeWhere((item) => item.product.id == productId);
    await _repo.save(_items);
  }

  double get total => _items.fold(
    0,
    (sum, item) => sum + (item.product.price * item.quantity),
  );
}

// test/cart_bloc_test.dart
void main() {
  late MockCartRepository mockRepo;
  late CartBloc bloc;

  setUp(() {
    mockRepo = MockCartRepository();
    when(mockRepo.save(any)).thenAnswer((_) async {});
    bloc = CartBloc(mockRepo);
  });

  test('add item updates list and persists', () async {
    final product = Product(id: '1', name: 'Widget', price: 9.99);

    await bloc.addItem(product);

    expect(bloc.items.length, 1);
    expect(bloc.items.first.product.id, '1');
    verify(mockRepo.save(any)).called(1);
  });

  test('remove item clears list', () async {
    final product = Product(id: '1', name: 'Widget', price: 9.99);
    await bloc.addItem(product);

    await bloc.removeItem('1');

    expect(bloc.items, isEmpty);
  });

  test('total calculates correctly', () async {
    await bloc.addItem(Product(id: '1', name: 'A', price: 10.0));
    await bloc.addItem(Product(id: '2', name: 'B', price: 25.5));

    expect(bloc.total, 35.5);
  });
}

Widget Testing UI Components

Widget tests render components in a simulated environment. They’re slower than unit tests but verify your UI behaves correctly.

Basic Widget Test

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/counter_display.dart';

void main() {
  testWidgets('displays current count', (tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: CounterDisplay(count: 5),
      ),
    );

    expect(find.text('5'), findsOneWidget);
  });

  testWidgets('increments when plus tapped', (tester) async {
    int count = 0;

    await tester.pumpWidget(
      MaterialApp(
        home: StatefulBuilder(
          builder: (context, setState) {
            return CounterDisplay(
              count: count,
              onIncrement: () => setState(() => count++),
            );
          },
        ),
      ),
    );

    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    expect(count, 1);
  });
}

Testing Form Validation

testWidgets('shows error for empty email', (tester) async {
  await tester.pumpWidget(
    MaterialApp(
      home: Scaffold(
        body: LoginForm(),
      ),
    ),
  );

  await tester.tap(find.byType(ElevatedButton));
  await tester.pumpAndSettle();

  expect(find.text('Email is required'), findsOneWidget);
});

testWidgets('shows error for invalid email', (tester) async {
  await tester.pumpWidget(
    MaterialApp(
      home: Scaffold(
        body: LoginForm(),
      ),
    ),
  );

  await tester.enterText(find.byType(TextFormField), 'notanemail');
  await tester.tap(find.byType(ElevatedButton));
  await tester.pumpAndSettle();

  expect(find.text('Enter a valid email'), findsOneWidget);
});

Testing Animations

testWidgets('fade in animation completes', (tester) async {
  await tester.pumpWidget(
    MaterialApp(
      home: FadeInWidget(child: Text('Hello')),
    ),
  );

  await tester.pump();
  expect(find.text('Hello'), findsNothing);

  await tester.pump(Duration(milliseconds: 500));
  expect(find.text('Hello'), findsOneWidget);
});

Golden Tests for Visual Regression

testWidgets('counter widget matches golden', (tester) async {
  await tester.pumpWidget(
    MaterialApp(
      home: CounterWidget(),
    ),
  );

  await expectLater(
    find.byType(CounterWidget),
    matchesGoldenFile('goldens/counter_widget.png'),
  );
});

Update goldens after intentional changes:

flutter test --update-goldens

Integration Testing Full Flows

Integration tests run on real devices or emulators, testing complete user journeys.

Project Setup

Create integration_test/app_test.dart:

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('End-to-End', () {
    testWidgets('complete login flow', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      await tester.enterText(find.byKey(Key('email_field')), 'user@test.com');
      await tester.enterText(find.byKey(Key('password_field')), 'password123');
      await tester.tap(find.byKey(Key('login_button')));
      await tester.pumpAndSettle();

      expect(find.text('Welcome'), findsOneWidget);
    });

    testWidgets('add item to cart and checkout', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      await tester.tap(find.byKey(Key('product_1')));
      await tester.pumpAndSettle();

      await tester.tap(find.byKey(Key('add_to_cart')));
      await tester.pumpAndSettle();

      await tester.tap(find.byKey(Key('cart_icon')));
      await tester.pumpAndSettle();

      expect(find.text('1 item'), findsOneWidget);

      await tester.tap(find.byKey(Key('checkout_button')));
      await tester.pumpAndSettle();

      expect(find.text('Order Confirmed'), findsOneWidget);
    });
  });
}

Run on Device

flutter test integration_test/app_test.dart

Testing Navigation

testWidgets('navigate to settings and back', (tester) async {
  app.main();
  await tester.pumpAndSettle();

  await tester.tap(find.byIcon(Icons.settings));
  await tester.pumpAndSettle();

  expect(find.text('Settings'), findsOneWidget);
  expect(find.byType(SettingsPage), findsOneWidget);

  await tester.tap(find.byIcon(Icons.arrow_back));
  await tester.pumpAndSettle();

  expect(find.byType(HomePage), findsOneWidget);
});

Test Organization and CI/CD

Project Structure

lib/
  models/
  services/
  widgets/
test/
  models/
    user_test.dart
  services/
    api_test.dart
  widgets/
    button_test.dart
  helpers/
    test_helpers.dart
integration_test/
  app_test.dart
  flows/
    auth_flow.dart
    purchase_flow.dart

Shared Test Utilities

Create test/helpers/test_helpers.dart:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

Widget buildTestable(Widget widget) {
  return MaterialApp(
    home: Scaffold(
      body: widget,
    ),
  );
}

Future<void> tapAndSettle(WidgetTester tester, Finder finder) async {
  await tester.tap(finder);
  await tester.pumpAndSettle();
}

GitHub Actions CI

Create .github/workflows/test.yml:

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.19.0'

      - name: Install dependencies
        run: flutter pub get

      - name: Run unit tests
        run: flutter test --coverage

      - name: Run widget tests
        run: flutter test test/

      - name: Run integration tests
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 31
          script: flutter test integration_test/

Code Coverage

Generate and view coverage:

flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html

Target 80%+ coverage on business logic, 60%+ on UI.

Common Testing Patterns

Test Data Builders

class UserBuilder {
  String _id = '1';
  String _name = 'Test User';
  String _email = 'test@example.com';

  UserBuilder withId(String id) => _id = id;
  UserBuilder withName(String name) => _name = name;
  UserBuilder withEmail(String email) => _email = email;

  User build() => User(id: _id, name: _name, email: _email);
}

Parameterized Tests

void main() {
  final testCases = [
    ('', 'Email is required'),
    ('invalid', 'Enter a valid email'),
    ('user@', 'Enter a valid email'),
    ('user@test.com', null),
  ];

  for (final (input, expectedError) in testCases) {
    test('validates email: $input', () {
      final error = validateEmail(input);
      expect(error, expectedError);
    });
  }
}

Conclusion

Testing isn’t a phase—it’s a practice woven into every commit. Start with unit tests for business logic, add widget tests for critical UI paths, and cap with integration tests for key user flows.

Flutter’s testing tools are mature and fast. The investment pays dividends in fewer bugs, faster releases, and code you can refactor without fear.

Next steps:

  • Add tests to your current project starting with the most critical features
  • Set up CI to run tests on every PR
  • Track coverage trends over time
  • Explore Patrol for native integration testing