Repository: firebase/firebase-tools
Pull request #1425 (2019-06-20 20:11:58)

add end-to-end test for emulator function triggers
<!--

Thank you for contributing to the Firebase community! Please fill out the form below.

Run the linter and test suite
==============================
Run `npm test` to make sure your changes compile properly and the tests all pass on your local machine. We've hooked up this repo with continuous integration to double check those things for you. 

-->


### Description

<!-- Are you fixing a bug? Implementing a new feature? Make sure we have the context around your change. Link to other relevant issues or pull requests. -->

This tests checks that RTDB and Firestore emulator write triggers work, and that the admin SDK can be used from within such a triggered function to write back to either the RTDB emulator or the Firestore emulator. See:

- https://github.com/firebase/firebase-tools/pull/1347
- https://github.com/firebase/firebase-tools/pull/1411

for more context.

I created a new firebase project called `fir-tools-testing` to run this test, but I'm not sure if that's necessary (or if we have a different project we use for CI tests -- if that is the case I will migrate this test over). 

This test is just a starting point, I plan to add more coverage in the future by adding new driver programs in the `firebase-tools/triggers-end-to-end-tests` directory in the future. 

TODO: I have yet to create/test a new travis job for this test.
TODO: This PR depends on changes made in https://github.com/firebase/firebase-tools/pull/1411, so I will rebase this onto master once https://github.com/firebase/firebase-tools/pull/1411 goes in.
	 
### Scenarios Tested

<!-- Write a list of all the user journeys and edge cases you've tested. Instructions for manual testing can be found at https://github.com/firebase/firebase-tools/blob/master/.github/CONTRIBUTING.md#development-setup -->

### Sample Commands

<!-- Proposing a change to commands or flags? Provide examples of how they will be used. -->

`cd firebase-tools/scripts/triggers-end-to-end-tests && ./test.js`


coveralls: 
[![Coverage Status](https://coveralls.io/builds/24116776/badge)](https://coveralls.io/builds/24116776)

Coverage decreased (-0.05%) to 61.926% when pulling **d6ee01ff8adb1c8f37679a82732df4db922cdf01 on wyszynski/emulator-triggers-end-to-end-test** into **c54ac53a902fa1382ed99b0ce589862defe7a1d2 on ss-activate-rtdb-triggers**.

ryanpbrewster: and how to run it
samtstern: @IanWyszynski #1411 has been merged into master.

Patch:
diff --git a/.travis.yml b/.travis.yml
index 378e72b40..a93ce92e5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,6 +12,12 @@ jobs:
       before_script: ./scripts/decrypt-app-credentials.sh
       script: ./scripts/test-hosting.sh
       after_script: skip
+    - stage: emulator triggers end to end test
+      if: repo == head_repo OR type == push
+      node_js: "8"
+      before_script: skip
+      script: ./scripts/test-triggers-end-to-end.sh
+      after_script: skip
 cache:
   directories:
     - node_modules


Test patch:
diff --git a/scripts/test-triggers-end-to-end.sh b/scripts/test-triggers-end-to-end.sh
new file mode 100755
index 000000000..157e48bb0
--- /dev/null
+++ b/scripts/test-triggers-end-to-end.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+set -xe
+
+FIREBASE_CLI="./lib/bin/firebase.js"
+
+if ! [ -x $FIREBASE_CLI ];
+then
+  echo "marking $FIREBASE_CLI user/group executable"
+  chmod ug+x $FIREBASE_CLI
+fi;
+
+$FIREBASE_CLI setup:emulators:firestore
+$FIREBASE_CLI setup:emulators:database
+
+cd ./scripts/triggers-end-to-end-tests
+
+npm install && npm test
diff --git a/scripts/triggers-end-to-end-tests/.firebaserc b/scripts/triggers-end-to-end-tests/.firebaserc
new file mode 100644
index 000000000..f7b55c6f2
--- /dev/null
+++ b/scripts/triggers-end-to-end-tests/.firebaserc
@@ -0,0 +1,5 @@
+{
+  "projects": {
+    "default": "fir-tools-testing"
+  }
+}
diff --git a/scripts/triggers-end-to-end-tests/.gitignore b/scripts/triggers-end-to-end-tests/.gitignore
new file mode 100644
index 000000000..a73226533
--- /dev/null
+++ b/scripts/triggers-end-to-end-tests/.gitignore
@@ -0,0 +1,67 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+firebase-debug.log*
+database-debug.log*
+firestore-debug.log*
+
+# Firebase cache
+.firebase/
+
+# Firebase config
+
+# Uncomment this if you'd like others to create their own Firebase project.
+# For a team working on the same Firebase project(s), it is recommended to leave
+# it commented so all members can deploy to the same project(s) in .firebaserc.
+# .firebaserc
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
diff --git a/scripts/triggers-end-to-end-tests/README.md b/scripts/triggers-end-to-end-tests/README.md
new file mode 100644
index 000000000..f7c7dd3e1
--- /dev/null
+++ b/scripts/triggers-end-to-end-tests/README.md
@@ -0,0 +1,86 @@
+# End-to-end Tests for Emulator Function Triggers
+
+This test exercises support for Realtime Database function triggers as
+introduced in the following PRs:
+
+- https://github.com/firebase/firebase-tools/pull/1347
+- https://github.com/firebase/firebase-tools/pull/1411
+
+# Running Instructions
+
+Install dependencies:
+```
+cd firebase-tools/scripts/triggers-end-to-end-tests && npm install
+```
+
+Run the test:
+```
+$ cd firebase-tools/scripts/triggers-end-to-end-tests && npm test
+```
+
+This end-to-end test uses the mocha testing framework.
+
+## Verification
+
+The test spins up a functions, database, and firestore emulator to verify the
+following:
+
+1. The functions emulator (firebase-tools >= v7.0.0) can register
+   [triggers](https://firebase.google.com/docs/functions/database-events)
+   with database emulators (>= v4.0.0).
+2. The database emulator (>= v4.0.0) can invoke functions served by a local
+   functions emulator instance.
+3. Functions served by the functions emulator may be defined to operate on a
+   local database emulator instance using the
+   [node.js admin SDK](https://github.com/firebase/firebase-admin-node) (and are
+   configured to do so by default).
+4. Local functions triggered by a database emulator event may operate on a local
+   firestore emulator instance using the node.js admin SDK.
+5. Local functions triggered by a firestore emulator event may operate on both local
+   database emulators (>= v4.0.0) and the invoking firestore emulator using the
+   node.js admin SDK.
+
+At a high level, this test verifies the use of cloud functions as a
+bidirectional communication channel between the database emulator and Firestore.
+
+## Implementation
+
+To verify (2), the test installs the following http function:
+
+```javascript
+exports.writeToRtdb = functions.https.onRequest(async (req, res) => {
+    const ref = admin.database().ref(START_DOCUMENT_NAME);
+    await ref.set({ start: (new Date().toISOString()) });
+    ref.once('value', (snap) => {
+        res.json({ data: snap });
+    });
+});
+```
+
+This function performs a document write that triggers the following realtime
+database function:
+
+```javascript
+exports.rtdbReaction = functions.database.ref(START_DOCUMENT_NAME).onWrite(async (change, ctx) => {
+    console.log(RTDB_FUNCTION_LOG);
+
+    const ref = admin.database().ref(END_DOCUMENT_NAME + "_from_database");
+    await ref.set({ done: (new Date().toISOString()) });
+
+    const firestoreref = admin.firestore().doc(END_DOCUMENT_NAME + "_from_database");
+    await firestoreref.set({ done: (new Date().toISOString()) });
+
+    return true;
+});
+```
+The driver program for the end-to-end test has spawned the functions emulator as
+a subprocess and searches for the presence of `RTDB_FUNCTION_LOG` in the process
+to confirm (2).
+
+In addition to printing a marker, the above database function also uses the
+admin SDK to write "completion markers" back to the realtime database and
+firestore emulators and a local firestore emulator. The test uses the presence of
+these markers is checked by the driver program to confirm (3) and (4).
+
+(5) is confirmed using analogous http and firestore-triggered functions. For
+brevity, they are not reproduced here.
diff --git a/scripts/triggers-end-to-end-tests/firebase.json b/scripts/triggers-end-to-end-tests/firebase.json
new file mode 100644
index 000000000..f16f1f586
--- /dev/null
+++ b/scripts/triggers-end-to-end-tests/firebase.json
@@ -0,0 +1,18 @@
+{
+  "database": {},
+  "firestore": {
+    "rules": "firestore.rules"
+  },
+  "functions": {},
+  "emulators": {
+    "database": {
+      "port": 9000
+    },
+    "firestore": {
+      "port": 9001
+    },
+    "functions": {
+      "port": 9002
+    }
+  }
+}
diff --git a/scripts/triggers-end-to-end-tests/functions/.gitignore b/scripts/triggers-end-to-end-tests/functions/.gitignore
new file mode 100644
index 000000000..884afa60c
--- /dev/null
+++ b/scripts/triggers-end-to-end-tests/functions/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+.eslintrc
+package-lock.json
diff --git a/scripts/triggers-end-to-end-tests/functions/index.js b/scripts/triggers-end-to-end-tests/functions/index.js
new file mode 100644
index 000000000..a23382fa7
--- /dev/null
+++ b/scripts/triggers-end-to-end-tests/functions/index.js
@@ -0,0 +1,78 @@
+const admin = require("firebase-admin");
+const functions = require("firebase-functions");
+
+/*
+ * Log snippets that the driver program above checks for. Be sure to update
+ * ../test.js if you plan on changing these.
+ */
+const RTDB_FUNCTION_LOG = '========== RTDB FUNCTION ==========';
+const FIRESTORE_FUNCTION_LOG = '========== FIRESTORE FUNCTION ==========';
+
+/*
+ * We install onWrite triggers for START_DOCUMENT_NAME in both the firestore and
+ * database emulators. From each respective onWrite trigger, we write a document
+ * to both the firestore and database emulators. This exercises the
+ * bidirectional communication between cloud functions and each emulator.
+ */
+const START_DOCUMENT_NAME = "test/start";
+const END_DOCUMENT_NAME = "test/done";
+
+admin.initializeApp();
+
+exports.deleteFromFirestore = functions.https.onRequest(async (req, res) => {
+    await admin.firestore().doc(START_DOCUMENT_NAME).delete();
+    res.json({ deleted: true });
+});
+
+exports.deleteFromRtdb = functions.https.onRequest(async (req, res) => {
+    await admin.database().ref(START_DOCUMENT_NAME).remove();
+    res.json({ deleted: true });
+});
+
+exports.writeToFirestore = functions.https.onRequest(async (req, res) => {
+    const ref = admin.firestore().doc(START_DOCUMENT_NAME);
+    await ref.set({ start: (new Date().toISOString()) });
+    ref.get().then((snap) => {
+        res.json({ data: snap.data() });
+    });
+});
+
+exports.writeToRtdb = functions.https.onRequest(async (req, res) => {
+    const ref = admin.database().ref(START_DOCUMENT_NAME);
+    await ref.set({ start: (new Date().toISOString()) });
+    ref.once('value', (snap) => {
+        res.json({ data: snap });
+    });
+});
+
+exports.firestoreReaction = functions.firestore.document(START_DOCUMENT_NAME).onWrite(async (change, ctx) => {
+    console.log(FIRESTORE_FUNCTION_LOG);
+    /*
+     * Write back a completion timestamp to the firestore emulator. The test
+     * driver program checks for this by querying the firestore emulator
+     * directly.
+     */
+    const ref = admin.firestore().doc(END_DOCUMENT_NAME + "_from_firestore");
+    await ref.set({ done: (new Date().toISOString()) });
+
+    /*
+     * Write a completion marker to the firestore emulator. This exercise
+     * cross-emulator communication.
+     */
+    const dbref = admin.database().ref(END_DOCUMENT_NAME + "_from_firestore");
+    await dbref.set({ done: (new Date().toISOString()) });
+
+    return true;
+});
+
+exports.rtdbReaction = functions.database.ref(START_DOCUMENT_NAME).onWrite(async (change, ctx) => {
+    console.log(RTDB_FUNCTION_LOG);
+
+    const ref = admin.database().ref(END_DOCUMENT_NAME + "_from_database");
+    await ref.set({ done: (new Date().toISOString()) });
+
+    const firestoreref = admin.firestore().doc(END_DOCUMENT_NAME + "_from_database");
+    await firestoreref.set({ done: (new Date().toISOString()) });
+
+    return true;
+});
diff --git a/scripts/triggers-end-to-end-tests/functions/package.json b/scripts/triggers-end-to-end-tests/functions/package.json
new file mode 100644
index 000000000..5f51182a3
--- /dev/null
+++ b/scripts/triggers-end-to-end-tests/functions/package.json
@@ -0,0 +1,22 @@
+{
+  "name": "functions",
+  "description": "Cloud Functions for Firebase",
+  "scripts": {
+    "serve": "firebase serve --only functions",
+    "shell": "firebase functions:shell",
+    "start": "npm run shell",
+    "deploy": "firebase deploy --only functions",
+    "logs": "firebase functions:log"
+  },
+  "engines": {
+    "node": "8"
+  },
+  "dependencies": {
+    "firebase-admin": "^8.0.0",
+    "firebase-functions": "^3.0.0"
+  },
+  "devDependencies": {
+    "firebase-functions-test": "^0.1.6"
+  },
+  "private": true
+}
diff --git a/scripts/triggers-end-to-end-tests/package.json b/scripts/triggers-end-to-end-tests/package.json
new file mode 100644
index 000000000..5d431955c
--- /dev/null
+++ b/scripts/triggers-end-to-end-tests/package.json
@@ -0,0 +1,16 @@
+{
+  "name": "triggers-end-to-end-tests",
+  "version": "1.0.0",
+  "description": "End-to-end tests for emulator cloud function triggers",
+  "main": "run.spec.js",
+  "scripts": {
+    "test": "./node_modules/mocha/bin/mocha run.spec.js --reporter spec --exit"
+  },
+  "dependencies": {
+    "async": "^3.0.1",
+    "grpc": "^1.21.0",
+    "mocha": "^5.0.5"
+  },
+  "author": "",
+  "license": "ISC"
+}
diff --git a/scripts/triggers-end-to-end-tests/run.spec.js b/scripts/triggers-end-to-end-tests/run.spec.js
new file mode 100755
index 000000000..247ed9e92
--- /dev/null
+++ b/scripts/triggers-end-to-end-tests/run.spec.js
@@ -0,0 +1,310 @@
+#!/usr/bin/env node
+
+const admin = require("firebase-admin");
+const async = require("async");
+const chai = require('chai');
+const expect = chai.expect;
+const assert = chai.assert;
+const fs = require("fs");
+
+const Firestore = require("@google-cloud/firestore");
+const grpc = require("@grpc/grpc-js");
+
+const path = require("path");
+const request = require("request");
+const subprocess = require("child_process");
+
+const PROJECT_ROOT = path.dirname(path.dirname(path.dirname(__filename)));
+
+const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT;
+const FIREBASE_PROJECT_ZONE = "us-central1";
+
+/*
+ * Markers this test looks for in the emulator process stdout
+ * as one test for whether a cloud function was triggered.
+ */
+const RTDB_FUNCTION_LOG = "========== RTDB FUNCTION ==========";
+const FIRESTORE_FUNCTION_LOG = "========== FIRESTORE FUNCTION ==========";
+
+/*
+ * Various delays that are needed because this test spawns
+ * parallel emulator subprocesses.
+ */
+const TEST_SETUP_TIMEOUT = 20000;
+const EMULATORS_STARTUP_DELAY_MS = 7000;
+const EMULATORS_WRITE_DELAY_MS = 5000;
+const EMULATORS_SHUTDOWN_DELAY_MS = 2000;
+
+/*
+ * Realtime Database and Firestore documents we used to verify
+ * bidirectional communication between the two via cloud functions.
+ */
+const FIRESTORE_COMPLETION_MARKER = "test/done_from_firestore";
+const DATABASE_COMPLETION_MARKER = "test/done_from_database";
+
+function TriggerEndToEndTest(config)
+{
+  this.rtdb_emulator_host = "localhost";
+  this.rtdb_emulator_port = config.emulators.database.port;
+
+  this.firestore_emulator_host = "localhost";
+  this.firestore_emulator_port = config.emulators.firestore.port;
+
+  this.functions_emulator_host = "localhost";
+  this.functions_emulator_port = config.emulators.functions.port;
+
+  this.rtdb_trigger_count = 0;
+  this.firestore_trigger_count = 0;
+
+  this.rtdb_from_firestore = false;
+  this.firestore_from_rtdb = false;
+
+  this.rtdb_from_rtdb = false;
+  this.firestore_from_firestore = false;
+
+  this.emulators_process = null;
+}
+
+/*
+ * Check that all directions of database <-> functions <-> firestore
+ * worked.
+ */
+TriggerEndToEndTest.prototype.success = function success()
+{
+  return this.rtdb_from_firestore &&
+      this.rtdb_from_rtdb &&
+      this.firestore_from_firestore &&
+      this.firestore_from_rtdb;
+};
+
+TriggerEndToEndTest.prototype.startEmulators = function startEmulators()
+{
+  var self = this;
+  self.emulators_process = subprocess.spawn("node", [
+      PROJECT_ROOT + "/lib/bin/firebase.js",
+      "emulators:start",
+      "--project",
+      FIREBASE_PROJECT
+  ]);
+
+  self.emulators_process.stdout.on("data", function (data) {
+    process.stdout.write("[emulators stdout] " + data);
+    if (data.indexOf(RTDB_FUNCTION_LOG) > -1) {
+      self.rtdb_trigger_count++;
+    }
+    if (data.indexOf(FIRESTORE_FUNCTION_LOG) > -1) {
+      self.firestore_trigger_count++;
+    }
+  });
+
+  self.emulators_process.stderr.on("data", function (data) {
+    console.log("[emulators stderr] " + data);
+  });
+}
+
+TriggerEndToEndTest.prototype.stopEmulators = function stopEmulators(done)
+{
+  this.emulators_process.once('close', function (exitCode, signal) {
+    done();
+  });
+
+  /*
+   * CLI process only shuts down emulators cleanly on SIGINT.
+   */
+  this.emulators_process.kill("SIGINT");
+}
+
+TriggerEndToEndTest.prototype.writeToRtdb = function writeToRtdb(done)
+{
+  var url = "http://localhost:" + [
+      this.functions_emulator_port,
+      FIREBASE_PROJECT,
+      FIREBASE_PROJECT_ZONE,
+      "writeToRtdb"
+  ].join("/");
+
+  const req = request.get(url);
+
+  req.once('response', function(response) {
+    done(null, response);
+  });
+
+  req.once("error", function (err) {
+    done(err);
+  });
+};
+
+TriggerEndToEndTest.prototype.writeToFirestore = function writeToFirestore(done)
+{
+  var url = "http://localhost:" + [
+      this.functions_emulator_port,
+      FIREBASE_PROJECT,
+      FIREBASE_PROJECT_ZONE,
+      "writeToFirestore"
+  ].join("/");
+
+  const req = request.get(url);
+
+  req.once('response', function(response) {
+    done(null, response);
+  });
+  req.once('error', function (err) {
+    done(err);
+  });
+};
+
+function readConfig(done)
+{
+  fs.readFile("firebase.json", function (err, data) {
+    if (err) {
+      done(err);
+      return;
+    }
+    var config;
+    try {
+      config = JSON.parse(data);
+    } catch (err) {
+      done(err);
+      return;
+    }
+    done(null, config);
+  });
+}
+
+describe("database and firestore emulator function triggers", function () {
+  var test;
+
+  before(function (done) {
+    expect(FIREBASE_PROJECT).to.not.be.an('undefined');
+    expect(FIREBASE_PROJECT).to.not.be.null;
+    this.timeout(TEST_SETUP_TIMEOUT);
+    async.series([
+      function (done) {
+        readConfig(function (err, config) {
+          if (err) {
+            done(new Error("error reading firebase.json: " + err));
+            return;
+          }
+          test = new TriggerEndToEndTest(config);
+          done();
+        });
+      },
+      function (done) {
+          test.startEmulators();
+          /*
+           * Give some time for the emulator subprocesses to start up.
+           */
+          setTimeout(done, EMULATORS_STARTUP_DELAY_MS);
+      },
+      function (done) {
+          test.firestore_client = new Firestore({
+            port: test.firestore_emulator_port,
+            projectId: FIREBASE_PROJECT,
+            servicePath: 'localhost',
+            ssl: false
+          });
+
+          admin.initializeApp({
+            projectId: FIREBASE_PROJECT,
+            databaseURL: "http://localhost:" + test.rtdb_emulator_port + "?ns=" + FIREBASE_PROJECT,
+            credential: {
+              getAccessToken: () => {
+                return Promise.resolve({
+                  expires_in: 1000000,
+                  access_token: "owner",
+                });
+              },
+              getCertificate: () => {
+                return {};
+              }
+            }
+          });
+
+          test.database_client = admin.database();
+          done();
+      },
+      function (done) {
+        const firestore = test.firestore_client;
+        const database = test.database_client;
+
+        /*
+         * Install completion marker handlers and have them update state
+         * in the global test fixture on success. We will later check that
+         * state to determine whether the test passed or failed.
+         */
+        database.ref(FIRESTORE_COMPLETION_MARKER).on("value", function (snap) {
+          test.rtdb_from_firestore = true;
+        }, function (err) {
+          assert.fail(err, "Error reading " + FIRESTORE_COMPLETION_MARKER + " from database emulator.");
+        });
+
+        database.ref(DATABASE_COMPLETION_MARKER).on("value", function (snap) {
+          test.rtdb_from_rtdb = true;
+        }, function (err) {
+          assert.fail(err, "Error reading " + DATABASE_COMPLETION_MARKER + " from database emulator.");
+        });
+
+        firestore.doc(FIRESTORE_COMPLETION_MARKER).onSnapshot(function (snap) {
+          test.firestore_from_firestore = true;
+        }, function (err) {
+          assert.fail(err, "Error reading " + FIRESTORE_COMPLETION_MARKER + " from firestore emulator.");
+        });
+
+        firestore.doc(DATABASE_COMPLETION_MARKER).onSnapshot(function (snap) {
+          test.firestore_from_rtdb = true;
+        }, function (err) {
+          assert.fail(err, "Error reading " + DATABASE_COMPLETION_MARKER + " from firestore emulator.");
+        });
+        done();
+      }
+    ], done);
+  });
+
+  it("should write to the database emulator", function (done) {
+    this.timeout(EMULATORS_WRITE_DELAY_MS);
+
+    test.writeToRtdb(function (err, response) {
+      expect(err).to.be.null;
+      expect(response.statusCode).to.equal(200);
+      done(err);
+    });
+  });
+
+  it("should write to the firestore emulator", function (done) {
+    this.timeout(EMULATORS_WRITE_DELAY_MS * 2);
+
+    test.writeToFirestore(function (err, response) {
+      expect(err).to.be.null;
+      expect(response.statusCode).to.equal(200);
+
+      /*
+       * We delay again here because the functions triggered
+       * by the previous two writes run parallel to this and
+       * we need to give them and previous installed test
+       * fixture state handlers to complete before we check
+       * that state in the next test.
+       */
+      setTimeout(done, EMULATORS_WRITE_DELAY_MS);
+    });
+  });
+
+  it("should have have triggered cloud functions", function (done) {
+    expect(test.rtdb_trigger_count).to.equal(1);
+    expect(test.firestore_trigger_count).to.equal(1);
+    /*
+     * Check for the presence of all expected documents in the firestore
+     * and database emulators.
+     */
+    expect(test.success()).to.equal(true);
+    done();
+  });
+
+  after(function (done) {
+    this.timeout(EMULATORS_SHUTDOWN_DELAY_MS);
+    if (test) {
+      test.stopEmulators(done);
+      return;
+    }
+    done();
+  });
+});