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.