Contents:
- Testing Security Rules
- Testing Cloud Functions
- Priming with JSON data
- Why immutability matters...
For testing security rules, the library provides an immutable way of testing Firestore operations. This means your test data is not modified - you merely get the information whether it would be modified, by such operation and authentication.
Because of this, the tests can now be written in a simpler way.
import {
collection,
serverTimestamp,
deleteField,
arrayRemove,
arrayUnion
} from 'firebase-jest-testing/firestoreRules'
Your normal test might not need all the above imports - they are listed here for completeness.
collection
is a CollectionRef
-like interface (not the full thing!):
{
collection: collectionPath => {
as: { uid: string }|null => CollectionReference -like
}
}
Setting the access role happens at the collection level. An example:
describe("'/invites' rules", () => {
let unauth_invitesC, abc_invitesC, def_invitesC;
beforeAll( () => {
const coll = collection('invites');
unauth_invitesC = coll.as(null);
abc_invitesC = coll.as({ uid: 'abc' });
def_invitesC = coll.as({ uid: 'def' });
});
...
This code prepares variants of the same collection: one unauthenticated, and two with varying users supposed to have signed in. These can then be used throughout the tests, to check who has access.
Note: the now taken order of collection first is simply an API decision. This style seems to match practical use cases better than the reverse, since there's normally one collection but multiple users, per a test suite. More choices can be offered in the future, if use cases warrant the need for them.
You can use the provided CollectionReference
-like handles in the normal Firebase fashion:
expect( unauth_invitesC.get() ).toDeny()
method | description |
---|---|
.get() |
Check whether reading any document within the collection is allowed (i.e. can the user subscribe to it). |
.get(docName) |
Check whether reading a specific document is allowed. Same as .doc(docName).get() . |
.doc(docName) |
Get a DocumentReference -like handle. |
method | description |
---|---|
.get() |
Check whether reading the document is allowed. |
.set(any) |
Check whether writing the document, with the provided data, is allowed. |
.update(any) |
Check whether merging with the existing document is allowed. |
.delete() |
Check whether deleting the document is allowed. |
These methods try to be careful reproductions of the Firebase JS client SDK's similar methods. However, underneath there is no client - just REST API calls that interact with the Firebase Emulators.
The Firebase JS SDK provides server-side modifiers in the form of FieldValue.
Here are the look-alikes:
use | purpose | |
---|---|---|
serverTimestamp |
serverTimestamp() |
Time of the request |
deleteField |
deleteField() |
Removes the field |
arrayRemove |
arrayRemove("a","b") |
Removes certain values from an array |
arrayUnion |
arrayUnion("a","b") |
Adds certain values to an array, unless they already exist |
The use of these should be exactly as with a Firebase client.
Not all FieldValue
s have a corresponding look-alike. Only the ones deemed essential for testing Security Rules are implemented.
Note:
serverTimestamp()
anddeleteField()
always return the same sentinel value. The API has been kept as a function nonetheless, to match the Firebase JS SDK API.
Note: Our aim is not to provide comprehensive reproduction of a whole client, but all the necessary elements for testing Security Rules, in a fashion that is as 1-to-1 with the latest client API as it makes sense.
Example:
import { test, expect, describe, beforeAll } from '@jest/globals'
import { collection, serverTimestamp } from 'firebase-jest-testing/firestoreRules'
describe("'/invites' rules", () => {
let abc_invitesC;
beforeAll( () => {
const coll = collection('invites');
abc_invitesC = coll.as({uid:'abc'});
});
test('only a member of a project can invite', () => {
const d = { email: "aa@b.com", project: "1", by: "abc", at: serverTimestamp() };
return expect( abc_invitesC.doc("aa@b.com:1").set(d).toAllow() ) // author can invite
})
});
This is a simplified take on the test-rules/invitesC.test.js
. You can find the whole file (and more) in the project repo's sample folder.
While we are at the sample, notice the lack of Firebase specific setup.
The library picks up the configuration from firebase.json
automatically (if you have renamed it, set the FIREBASE_JSON
env.var). Just import the library and Jest.
The example has a .toAllow
, at the end of the test. Its counterpart, .toDeny
can be used to check a user does not have access.
These are Jest extensions, and they are automatically enabled by importing firestoreRules
.
Their use is self-explanatory. You have a Promise that should pass the security rules? expect
it .toAllow()
. It should not? .toDeny()
.
Note: The extensions were introduced by Jeff Delaney in Oct 2018: Testing Firestore Security Rules With the Emulator. The implementation here is different, but the API style is retained.
You can use either fashion. Promise.all
may increase the level of parallelism in your tests slightly, but there's no guarantee.
If you have lots of tests, and do performance comparisons between
Promise.all
vs. sequentialawait
s, let the author know how it went.
firebase-jest-testing
does not provide function-level testing tools for Cloud Functions. Instead, the aim is at end-to-end testing, where the tests set some aspect of the Firestore database, and remain listening whether changes are propagated elsewhere, as expected.
This part of the library uses firebase-admin
, which means Security Rules are not involved. The approach of the library is to test Security Rules separately, and forget about them even existing in other kinds of tests.
import {
collection,
doc
} from 'firebase-jest-testing/firestoreAdmin'
Sample:
describe("userInfo shadowing", () => {
beforeAll( () => {
preheat_EXP("projects/1/userInfo");
})
test('Central user information is distributed to a project where the user is a member', async () => {
const william = {
displayName: "William D.",
photoURL: "https://upload.wikimedia.org/wikipedia/commons/a/ab/Dalton_Bill-edit.png"
};
// Write in 'userInfo' -> causes a Cloud Function to update 'projectC/{project-id}/userInfo/{uid}'
//
await collection("userInfo").doc("abc").set(william);
await expect( docListener("projects/1/userInfo/abc") ).resolves.toContainObject(william);
});
...
The full example can be seen in sample/test-fns/userInfo.test.js
.
These are the CollectionReference
and DocumentReference
-like handles, and have only some of their methods exposed:
methods | |
---|---|
CollectionReference -like |
.doc |
DocumentReference -like |
.get , .set , .onSnapshot |
The library has configured them for emulator access, and will do cleanup for you.
If you need some further methods, contact the author with the use case.
Cloud Functions provide callable functions that are "similar but not identical to HTTP functions".
To exercise these callables, one normally needs a client-side SDK. firebase-admin
does not provide access to callables - it's not its thing.
firebase-jest-testing
uses the REST API and ducks the need for pulling in a client dependency. Its firebaseClientLike
interface tries to be close to that of the JS SDK, but it's not 100% the same.
import {
httpsCallable,
setRegion
} from 'firebase-jest-testing/firebaseClientLike'
Here's the whole sample test:
/*
* sample/test-fns/greet.test.js
*/
import { test, expect, describe, beforeAll } from '@jest/globals'
import { httpsCallable, setRegion } from 'firebase-jest-testing/firebaseClientLike'
const region = "mars-central2";
describe ('Cloud Function callables', () => {
beforeAll( () => {
setRegion(region)
});
test ('returns a greeting', async () => {
const msg = 'Jack';
const fnGreet = httpsCallable("greet");
const { data } = await fnGreet(msg);
expect(data).toBe("Greetings, Jack.");
});
});
- Cloud Functions can be run in regions.
- The Cloud Functions emulator is regions aware (since 9.12.0) and tests need to target a region that exists.
- Your functions can use no regions (defaults to
us-central1
), one or multiple. - We don't want your Cloud Functions implementation to have any special arrangements for testing.
- We cannot (without heuristics analysis of the Cloud Functions emulator logs) know in the tests, which regions your functions were set up to run on.
These statements lead us to the following advice, with regard to testing callables:
- If you run Cloud Functions in the default region (even if you also run them in other regions), you don't need to do anything special. Skip
setRegion
in the example. - If you only run in non-default region(s), specify one such region in the
setRegion
call.
This should cover most of the use cases.
If you have different implementations in different regions, and wish to test them separately, you can still use
setRegion
by calling it multiple times, but must take care of the execution order of such tests;setRegion
influences allhttpsCallable
tests following it as a global setting.
Like the Firebase JS SDK call of the same name, but without the first (fns
) parameter.
httpsCallable(name) // (string) => ((data) => Promise of { data: any|undefined, error: object|undefined })
Give the name of the callable, and then call the returned function with input data.
The returned promise should work as with the JS SDK client. If there are communication level problems (e.g. the named callable is not reached), the promise rejects.
Note: Roles are not currently implemented, but can be. It would be like:
httpsCallable(name).as({ uid: "you" })
. Let the author know (with a testable use case) if you need it.
Call this to set the region where your callables are running, under emulation.
setRegion(region) // (string) => ()
Only needed if one of the regions is not the Firebase default, or if your implementations vary between regions, and you wish to separately test them.
To run tests you need some seed data in Firestore. Such data is often hand crafted alongside the tests, and firebase-jest-testing
provides the means to read it from a JSON / .js
file.
Add this to your Jest configuration:
globalSetup: "./setup.jest.js"
In the setup.jest.js
:
/*
* Sets the (immutable) data for the Rules tests.
*/
import { docs } from './docs.js'
import { prime } from 'firebase-jest-testing/firestoreAdmin/setup'
const projectId = "rules-test"
async function setup() {
await prime(projectId, docs);
}
export default setup;
The project id (rules-test
) keeps unrelated tests apart in Firestore. The specific id does not really matter (but must be lower case, without spaces).
prime(projectId, docs) // (string, { <docPath>: any }) => Promise of ()
Calling prime
clears the earlier data contents, and primes the database with the docs
.
The docs are simply a flat object
with the doc path as the key.
Priming is done using firebase-admin
and it therefore bypasses any security rules.
If you have schema checks as part of the Security Rules, the seed data may be in breach of these rules. Be extra careful to manually craft the data so that it is valid.
Then again, maybe you need to test for earlier non-conforming data cases so this really is the way it should be.
As a particular case to watch for, create timestamps with new Date()
(or serverTimestamp()
). firebase-admin
converts them to Cloud Firestore timestamps, but this is not the case for and Date.now()
which return Unix epoch numbers.Date.parse
expression | value |
---|---|
new Date() |
Mon Aug 24 2020 17:16:58 GMT+0300 |
new Date('27 Mar 2020 14:17:00 GMT+0300') |
Fri Mar 27 2020 13:17:00 GMT+0200 |
Cloud Functions are not currently deactivated during the priming (we'll change that if it's needed).