Testing

How to test your migrations with Firebase emulator and testing frameworks.

Table of contents

  1. Firebase Emulator Setup
    1. Install Firebase Tools
    2. Initialize Firebase Emulator
    3. Configure Emulator
    4. Start Emulator
  2. Testing Migrations with Emulator
    1. Configure for Emulator
    2. Run Migrations Against Emulator
  3. Unit Testing Migrations
    1. Setup with Mocha/Chai
    2. Test Structure
    3. Run Tests
  4. Integration Testing
    1. Full Migration Flow Test
  5. Testing Best Practices
    1. 1. Use Fresh Database for Each Test
    2. 2. Test Both Up and Down
    3. 3. Test Edge Cases
    4. 4. Verify Data Integrity
  6. Automated Testing with npm Scripts
  7. CI/CD Integration
    1. GitHub Actions
  8. Testing with Real Data
    1. Create Test Fixtures
    2. Load Fixtures
  9. Performance Testing
    1. Measure Migration Time
    2. Test Large Datasets
  10. Debugging Tests
    1. Enable Verbose Logging
    2. Inspect Database State

Firebase Emulator Setup

Install Firebase Tools

npm install -g firebase-tools

Initialize Firebase Emulator

firebase init emulators

Select:

  • ✓ Realtime Database

Configure Emulator

Edit firebase.json:

{
  "emulators": {
    "database": {
      "port": 9000
    },
    "ui": {
      "enabled": true,
      "port": 4000
    }
  }
}

Start Emulator

firebase emulators:start --only database

Output:

✔  database: Emulator started at http://localhost:9000
✔  All emulators ready!

View Emulator UI at http://localhost:4000

Testing Migrations with Emulator

Configure for Emulator

// test-config.ts
import * as admin from 'firebase-admin';

export function initializeTestDatabase() {
  // Use emulator
  process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9000';

  admin.initializeApp({
    projectId: 'test-project',
    databaseURL: 'http://localhost:9000?ns=test-project'
  });

  return admin.database();
}

Run Migrations Against Emulator

import { FirebaseRunner, FirebaseConfig } from '@migration-script-runner/firebase';

async function testMigrations() {
  const appConfig = new FirebaseConfig();
  appConfig.folder = './migrations';
  appConfig.tableName = 'schema_version';
  appConfig.databaseUrl = 'http://localhost:9000';  // Emulator
  appConfig.applicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS;

  const runner = await FirebaseRunner.getInstance({ config: appConfig });

  // Run migrations
  const result = await runner.migrate();
  console.log('Migrations applied:', result.executed.length);

  // Verify data
  const db = runner.getDatabase();
  const snapshot = await db.ref('users').once('value');
  console.log('Users created:', snapshot.numChildren());
}

testMigrations();

Unit Testing Migrations

Setup with Mocha/Chai

npm install --save-dev mocha chai @types/mocha @types/chai

Test Structure

// test/migrations/001-create-users.test.ts
import { expect } from 'chai';
import * as admin from 'firebase-admin';
import { up, down } from '../../migrations/001-create-users';

describe('Migration 001: Create Users', () => {
  let db: admin.database.Database;

  before(() => {
    process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9000';
    admin.initializeApp({
      projectId: 'test-project',
      databaseURL: 'http://localhost:9000?ns=test-project'
    });
    db = admin.database();
  });

  afterEach(async () => {
    // Clean up after each test
    await db.ref().set(null);
  });

  after(async () => {
    await admin.app().delete();
  });

  describe('up()', () => {
    it('should create users node', async () => {
      await up(db);

      const snapshot = await db.ref('users').once('value');
      expect(snapshot.exists()).to.be.true;
    });

    it('should create user1', async () => {
      await up(db);

      const snapshot = await db.ref('users/user1').once('value');
      const user = snapshot.val();

      expect(user).to.exist;
      expect(user.name).to.equal('John Doe');
      expect(user.email).to.equal('john@example.com');
    });
  });

  describe('down()', () => {
    it('should remove users node', async () => {
      await up(db);
      await down(db);

      const snapshot = await db.ref('users').once('value');
      expect(snapshot.exists()).to.be.false;
    });
  });
});

Run Tests

# Start emulator
firebase emulators:start --only database

# Run tests (in another terminal)
npm test

Integration Testing

Full Migration Flow Test

// test/integration/migration-flow.test.ts
import { expect } from 'chai';
import { FirebaseRunner, FirebaseConfig } from '@migration-script-runner/firebase';
import * as admin from 'firebase-admin';

describe('Migration Flow', () => {
  let runner: FirebaseRunner;
  let db: admin.database.Database;

  before(async () => {
    process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9000';
    admin.initializeApp({
      projectId: 'test-project',
      databaseURL: 'http://localhost:9000?ns=test-project'
    });

    const appConfig = new FirebaseConfig();
    appConfig.folder = './migrations';
    appConfig.tableName = 'schema_version';
    appConfig.databaseUrl = 'http://localhost:9000?ns=test-project';

    runner = await FirebaseRunner.getInstance({ config: appConfig });
    db = runner.getDatabase();
  });

  afterEach(async () => {
    // Reset database
    await db.ref().set(null);

    // Reset migrations
    await runner.down({ to: 0 });
  });

  after(async () => {
    await admin.app().delete();
  });

  it('should apply all migrations', async () => {
    const result = await runner.migrate();

    expect(result.status).to.equal('success');
    expect(result.appliedMigrations.length).to.be.greaterThan(0);
  });

  it('should rollback migrations', async () => {
    await runner.migrate();

    const result = await runner.down();

    expect(result.status).to.equal('success');
  });

  it('should list migration status', async () => {
    await runner.migrate();

    const statuses = await runner.list();

    expect(statuses).to.be.an('array');
    expect(statuses.every(s => s.status === 'applied')).to.be.true;
  });
});

Testing Best Practices

1. Use Fresh Database for Each Test

afterEach(async () => {
  await db.ref().set(null);
});

2. Test Both Up and Down

describe('Migration', () => {
  it('should apply migration', async () => {
    await up(db);
    // Assertions...
  });

  it('should rollback migration', async () => {
    await up(db);
    await down(db);
    // Verify rollback...
  });
});

3. Test Edge Cases

it('should handle empty database', async () => {
  await up(db);
  // Migration should handle missing data gracefully
});

it('should be idempotent', async () => {
  await up(db);
  await up(db); // Run twice

  // Should produce same result
  const snapshot = await db.ref('users').once('value');
  expect(snapshot.numChildren()).to.equal(1);
});

4. Verify Data Integrity

it('should maintain data relationships', async () => {
  await up(db);

  const user = await db.ref('users/user1').once('value');
  const postAuthor = await db.ref('posts/post1/authorId').once('value');

  expect(postAuthor.val()).to.equal('user1');
});

Automated Testing with npm Scripts

Add to package.json:

{
  "scripts": {
    "test": "firebase emulators:exec --only database 'npm run test:mocha'",
    "test:mocha": "mocha --require ts-node/register 'test/**/*.test.ts'",
    "test:watch": "mocha --require ts-node/register --watch 'test/**/*.test.ts'"
  }
}

Run tests:

npm test

CI/CD Integration

GitHub Actions

name: Test Migrations

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm ci

      - name: Install Firebase Tools
        run: npm install -g firebase-tools

      - name: Run tests
        run: npm test

Testing with Real Data

Create Test Fixtures

// test/fixtures/users.json
{
  "user1": {
    "name": "John Doe",
    "email": "john@example.com"
  },
  "user2": {
    "name": "Jane Smith",
    "email": "jane@example.com"
  }
}

Load Fixtures

import { readFileSync } from 'fs';

beforeEach(async () => {
  const fixtures = JSON.parse(
    readFileSync('./test/fixtures/users.json', 'utf-8')
  );

  await db.ref('users').set(fixtures);
});

Performance Testing

Measure Migration Time

it('should complete migration within 5 seconds', async function() {
  this.timeout(5000);

  const start = Date.now();
  await runner.migrate();
  const duration = Date.now() - start;

  expect(duration).to.be.lessThan(5000);
});

Test Large Datasets

it('should handle 10000 records', async function() {
  this.timeout(30000);

  // Create large dataset
  const updates: Record<string, any> = {};
  for (let i = 0; i < 10000; i++) {
    updates[`users/user${i}`] = { name: `User ${i}` };
  }
  await db.ref().update(updates);

  // Run migration
  await up(db);

  // Verify
  const snapshot = await db.ref('users').once('value');
  expect(snapshot.numChildren()).to.equal(10000);
});

Debugging Tests

Enable Verbose Logging

import { FirebaseRunner, FirebaseConfig } from '@migration-script-runner/firebase';
import { ConsoleLogger } from '@migration-script-runner/core';

const appConfig = new FirebaseConfig();
appConfig.folder = './migrations';
appConfig.tableName = 'schema_version';
appConfig.databaseUrl = 'http://localhost:9000';

const runner = await FirebaseRunner.getInstance({
  config: appConfig,
  logger: new ConsoleLogger({ level: 'debug' })
});

Inspect Database State

afterEach(async () => {
  // Dump database state for debugging
  const snapshot = await db.ref().once('value');
  console.log('Database state:', JSON.stringify(snapshot.val(), null, 2));
});