Testing Flutter apps with Firebase emulators
By Ann Tech · 6 June 2026
Running integration tests against a live Firebase project is slow, expensive, and leaves test data in production. Firebase emulators solve all three problems — they run locally, cost nothing, and reset between test runs.
Install and configure emulators
npm install -g firebase-tools
firebase login
firebase init emulators
Select: Firestore, Authentication, Functions (and any others you use).
firebase.json:
{
"emulators": {
"auth": { "port": 9099 },
"firestore": { "port": 8080 },
"functions": { "port": 5001 },
"ui": { "enabled": true, "port": 4000 }
}
}
# Start emulators
firebase emulators:start
The Emulator UI at localhost:4000 shows real-time data, auth users, and function logs.
Connecting Flutter to emulators
Future<void> connectToEmulators() async {
const host = 'localhost'; // Use '10.0.2.2' for Android emulator
// Firestore
FirebaseFirestore.instance.useFirestoreEmulator(host, 8080);
// Auth
await FirebaseAuth.instance.useAuthEmulator(host, 9099);
// Functions
FirebaseFunctions.instance.useFunctionsEmulator(host, 5001);
// Storage
await FirebaseStorage.instance.useStorageEmulator(host, 9199);
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// Connect to emulators in debug/test mode only
if (kDebugMode || const bool.fromEnvironment('USE_EMULATORS')) {
await connectToEmulators();
}
runApp(const MyApp());
}
Integration tests with emulators
// integration_test/firestore_test.dart
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() async {
await Firebase.initializeApp();
FirebaseFirestore.instance.useFirestoreEmulator('localhost', 8080);
await FirebaseAuth.instance.useAuthEmulator('localhost', 9099);
});
setUp(() async {
// Clear emulator data between tests
await FirebaseFirestore.instance.clearPersistence();
// Sign out before each test
await FirebaseAuth.instance.signOut();
});
testWidgets('user can create and see an order', (tester) async {
// Create a test user in the Auth emulator
final credential = await FirebaseAuth.instance
.createUserWithEmailAndPassword(
email: '[email protected]',
password: 'password123',
);
// Seed test data
await FirebaseFirestore.instance
.collection('products')
.doc('product-1')
.set({'name': 'Test Product', 'price': 9.99});
// Launch app
app.main();
await tester.pumpAndSettle();
// Interact with the app
await tester.tap(find.byKey(const Key('product-1')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('add_to_cart')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('checkout')));
await tester.pumpAndSettle();
// Verify the order was created in Firestore
final orders = await FirebaseFirestore.instance
.collection('orders')
.where('userId', isEqualTo: credential.user!.uid)
.get();
expect(orders.docs.length, 1);
});
}
Running tests against emulators in CI
# .github/workflows/integration-tests.yml
jobs:
integration-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Firebase CLI
run: npm install -g firebase-tools
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
cache: true
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Start emulators and run tests
run: |
firebase emulators:exec --project demo-test "flutter test integration_test/ -d emulator-5554 --dart-define=USE_EMULATORS=true"
Seeding test data
// test/helpers/firebase_seed.dart
Future<void> seedProducts(FirebaseFirestore db) async {
final batch = db.batch();
for (final product in testProducts) {
batch.set(
db.collection('products').doc(product.id),
product.toJson(),
);
}
await batch.commit();
}
Future<void> seedSecurityRules(String projectId) async {
// Optionally load rules from firestore.rules file
}
Common pitfalls
Using localhost on Android emulator. Android emulators can't reach the host machine's localhost. Use 10.0.2.2 instead: useFirestoreEmulator('10.0.2.2', 8080).
Not clearing emulator data between tests. If one test creates data that a later test doesn't expect, tests fail intermittently. Always call FirebaseFirestore.instance.clearPersistence() in setUp.
Connecting to emulators in production. Gate emulator connections behind kDebugMode or a --dart-define flag. Shipping code that connects to localhost:8080 to production means users see empty data or connection errors.
Sign in to like, dislike, or report.