← Articles

Running Flutter tests in GitHub Actions

By Charlin Joe · 27 October 2024

Running Flutter tests in CI catches regressions before they reach users. A well-configured GitHub Actions workflow runs on every pull request, blocks merges when tests fail, and gives the team confidence to ship fast.

Basic workflow

# .github/workflows/test.yml
name: Tests

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.22.0'
          channel: 'stable'
          cache: true  # Cache Flutter SDK across runs

      - name: Install dependencies
        run: flutter pub get

      - name: Analyze
        run: flutter analyze --no-fatal-infos

      - name: Test
        run: flutter test --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          file: coverage/lcov.info

Caching for faster runs

Flutter downloads packages every run without a cache. Cache ~/.pub-cache:

- name: Cache pub dependencies
  uses: actions/cache@v4
  with:
    path: ~/.pub-cache
    key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}
    restore-keys: |
      ${{ runner.os }}-pub-

With caching, flutter pub get goes from 60–90 seconds to 3–5 seconds on cache hit.

Running specific test types

- name: Unit tests
  run: flutter test test/unit/ --coverage

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

# Integration tests need a device or emulator
- name: Start emulator
  uses: reactivecircus/android-emulator-runner@v2
  with:
    api-level: 34
    script: flutter test integration_test/ -d emulator-5554

Multi-platform matrix

Run tests across Flutter channels to catch breaking changes early:

strategy:
  matrix:
    flutter-version: ['3.19.0', '3.22.0']
    os: [ubuntu-latest, macos-latest]
  fail-fast: false  # Continue other matrix runs even if one fails

runs-on: ${{ matrix.os }}

steps:
  - uses: subosito/flutter-action@v2
    with:
      flutter-version: ${{ matrix.flutter-version }}

Test result reporting

Output test results as JUnit XML for GitHub's test summary:

- name: Test
  run: |
    flutter test --reporter json | tee test-results.json
    dart run junitreport:tojunit \
      --input test-results.json \
      --output test-results.xml

- name: Publish test results
  uses: EnricoMi/publish-unit-test-result-action@v2
  if: always()  # Run even if tests fail
  with:
    files: test-results.xml

Conditional steps

Only run expensive steps when tests pass:

jobs:
  test:
    runs-on: ubuntu-latest
    outputs:
      tests-passed: ${{ steps.test.outcome == 'success' }}
    steps:
      - id: test
        run: flutter test

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - run: flutter build apk --release

Code coverage enforcement

Fail the build if coverage drops below a threshold:

- name: Check coverage
  run: |
    COVERAGE=$(lcov --summary coverage/lcov.info 2>&1 | grep 'lines' | awk '{print $2}' | tr -d '%')
    echo "Coverage: $COVERAGE%"
    if (( $(echo "$COVERAGE < 80" | bc -l) )); then
      echo "Coverage $COVERAGE% is below 80% threshold"
      exit 1
    fi

Monorepo setup

For projects with multiple packages:

strategy:
  matrix:
    package:
      - packages/core
      - packages/ui
      - packages/api
      - app

steps:
  - name: Test ${{ matrix.package }}
    working-directory: ${{ matrix.package }}
    run: flutter test --coverage

Secrets for integration tests

Some integration tests need real credentials (Firebase, API keys). Store as GitHub secrets and expose to the workflow:

- name: Create .env
  run: |
    echo "FIREBASE_API_KEY=${{ secrets.FIREBASE_API_KEY_TEST }}" >> .env
    echo "API_BASE_URL=https://test.api.example.com" >> .env

- name: Integration tests
  run: flutter test integration_test/
  env:
    FLUTTER_TEST_FIREBASE_PROJECT: ${{ secrets.FIREBASE_PROJECT_TEST }}

Parallelizing slow test suites

Split tests across multiple runners:

strategy:
  matrix:
    shard: [1, 2, 3]

- name: Test (shard ${{ matrix.shard }}/3)
  run: |
    flutter test \
      --shard-index ${{ matrix.shard }} \
      --total-shards 3

Common pitfalls

Not pinning the Flutter version. channel: stable installs whatever is current stable — which changes. Pin to a specific version (flutter-version: '3.22.0') and update deliberately.

Tests that pass locally but fail in CI. Common causes: timezone differences (use DateTime.utc()), path separators (path.join() not '/'), or file system assumptions. Run tests with TZ=UTC to catch timezone issues locally.

Slow test suites blocking PRs. Widget tests with real images or network requests are slow. Mock all I/O in tests; use flutter_test's pumpAndSettle timeout parameter to catch infinite animations.

No --no-fatal-infos on analyze. Info-level lint warnings become errors in CI without this flag, breaking builds over style issues that don't affect correctness.

Sign in to like, dislike, or report.

Running Flutter tests in GitHub Actions — ANN Tech