# Write Tests for Firebase Cloud Functions

Yes. An agent writes tests that run your Cloud Functions against the Firebase emulators, covering the trigger and its failure paths, then opens a PR.

[Run this on Rush](https://cloud.getrush.ai/run/write-emulator-tests/firebase-functions/), in a Project that has your repository.

## What the agent does

The agent reads the function before choosing emulators, because the trigger decides which ones a test needs. In firebase/functions-samples, `Node/fcm-notifications/functions/index.js` exports `sendFollowerNotification`, built with `onValueWritten` from `firebase-functions/database` on the path `/followers/{followedUid}/{followerUid}`. That is a Realtime Database trigger, not Firestore, so the tests need the Database and Auth emulators. The function reads tokens from `/users/{followedUid}/notificationTokens`, looks up the follower with `auth.getUser`, and sends one message per token with `messaging.sendEach`.

It lists every path through that function, since each is a test: an unfollow, where `event.data.after.val()` is falsy and the function returns early; a followed user with no tokens; a follower whose profile has no `displayName`, so the body falls back to "Someone"; a send where all messages succeed; and a send where some fail, after which the function removes the tokens whose error is `messaging/invalid-registration-token`, `messaging/registration-token-not-registered`, or `messaging/invalid-argument` with the exact invalid-token message, and keeps the rest.

Then it sets up the tooling the sample implies. `functions/package.json` uses ESM (`"type": "module"`), targets Node 24, depends on `firebase-functions` 7.4 and `firebase-admin` 14.5, already lists `firebase-functions-test` 3.5 in `devDependencies`, and has a `pnpm-lock.yaml` but no test runner or `test` script. The agent adds Mocha, writes `functions/test/index.test.js`, wraps the function with `firebase-functions-test` in online mode against a `demo-` project ID so nothing can reach a real project, seeds the Database emulator with `admin.database().ref(...).set(...)` and the Auth emulator with `admin.auth().createUser(...)`, and builds each change event with `makeChange` and `makeDataSnapshot`.

Cloud Messaging has no emulator, so `messaging.sendEach` is the one call the tests replace, with a stub that returns a `BatchResponse` shaped like the real one, including per-token error codes. Everything else, the token read, the user lookup and the token removal, hits the emulators, and each test asserts on what the database holds afterwards. It runs the suite with `firebase emulators:exec --only database,auth --project demo-fcm "npm test"` until every path passes.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "In Node/fcm-notifications, write tests with firebase-functions-test that run sendFollowerNotification against the Realtime Database and Auth emulators, using a demo- project ID. Cover the trigger and its failure paths: an unfollow, a user with no notification tokens, a follower with no displayName, all messages sent, and a partial failure where invalid or unregistered tokens are removed from the database and the others kept. Cloud Messaging has no emulator, so stub only messaging.sendEach with a realistic BatchResponse; seed and assert everything else through the emulators. Add a test runner and a test script to functions/package.json, run the suite with firebase emulators:exec until it passes, and open a pull request.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

The pull request adds `functions/test/index.test.js` with one test per path, a `.mocharc.json` if the ESM setup needs one, `mocha` in `devDependencies` with the updated `pnpm-lock.yaml`, and two scripts: `test` for Mocha and `test:emulators` that wraps it in `firebase emulators:exec --only database,auth`. If `firebase.json` has no `emulators` block, it adds one with fixed ports for the Database and Auth emulators so the tests and CI agree. `index.js` does not change.

The description lists each test with the database state it seeds and what it asserts afterwards ("partial failure: three tokens, one `registration-token-not-registered`; that token's key is gone, the other two remain"), explains why `sendEach` is the only stub, and quotes the emulator run's output. The session's record has every `emulators:exec` with its logs.

A reviewer checks the stubbed `BatchResponse` matches the shape `firebase-admin` 14 returns, that the project ID starts with `demo-` so the emulators refuse to touch real resources, and that each test clears the database it seeded.

## Run it on every new function exported

A functions codebase grows one export at a time. A GitHub trigger on `pull_request.opened` can start this run for any pull request that changes `functions/index.js`, with a prompt asking for emulator tests of each new or changed export, so a trigger arrives with tests that exercise it against the emulators.

For a repository of many samples, like functions-samples with dozens of directories under `Node/`, send one request per directory, each naming its folder in `input`. Every run gets its own sandbox with its own emulator ports, so runs do not collide.

## Why run it on Rush?

The Firebase emulators need Java, the Firebase CLI, free ports and a project ID that cannot reach production, and a test that fails halfway can leave an emulator running in the background. On Rush the agent installs and runs all of that in its own sandbox, discards it afterwards, and your machine never starts an emulator or signs in to Firebase.

The pull request is backed by real runs: the session's record shows each `firebase emulators:exec` and the test output inside it, so a reviewer can see the tests read and wrote the emulated database rather than mocks. With one sandbox per run, a whole directory of functions can be covered at once, each sample with its own pull request.

## Frequently asked questions

### The sample says Firestore. Why does it use the Realtime Database emulator?

Because the code says so: `onValueWritten` comes from `firebase-functions/database`, which is the Realtime Database. The agent reads the import rather than the description, and the pull request notes the difference so nobody wires up the wrong emulator later.

### Why stub messaging but nothing else?

Cloud Messaging is the one service this function uses that the Emulator Suite does not emulate. Stubbing only `sendEach` keeps the database reads, the Auth lookup and the token cleanup real, which is where this function's logic is.

### Will it touch my real Firebase project?

No. The tests run under a `demo-` project ID, which the emulators treat as offline-only, and the run needs no Firebase credentials. Deploy scripts in `package.json` are left alone.

### Does it work with Jest instead of Mocha?

Yes. With no runner in the project it adds Mocha, the one Firebase's own testing guide uses. If your functions already use Jest or Vitest, it writes the tests for that runner and adds nothing new.
