@getcircuit/firestore-test
This package is comprised of a set of utilities to help you write tests for your Firestore code. It mainly provides two significant features:
- A set of utilities to help you seed Circuit's firestore data inside a
jest
test environment. - A tiny CLI to help you get the emulator going with the appropriate configuration for easy testing.
Usage
CLI
The CLI is a simple wrapper around the firebase
CLI, but with the project
set to test
and pointing to this package's firebase.json
configuration file.
firestore-test emulator:start
firestore-test emulator:exec "<command>"
Example:
firestore-test emulator:exec "jest"
Utilities
Testing utilities
export {
getTestFirestore,
getTestRefSelectors,
getUniqueTestRootRef,
waitForSnapshotListenersInSync,
FIREBASE_TEST_PROJECT_ID,
} from '@getcircuit/firestore-test'
-
getTestFirestore()
returns a Firestore
instance that is connected to the emulator.
-
getTestRefSelectors()
returns a bag of ref selectors already attached to the test Firestore.
-
getUniqueTestRootRef()
returns a DocumentReference
that is unique to the current test. This is useful when you want to seed data that is specific to the current test without having to seed a whole team.
-
FIREBASE_TEST_PROJECT_ID
is the id of the test project. It's _firebase_test_
currently.
import { getUniqueTestRootRef } from '@getcircuit/firestore-test'
const ref = getUniqueTestRootRef()
waitForSnapshotListenersInSync()
is a function that returns a promise that resolves when all active snapshot listeners are in sync with the remote firestore. This is useful when you want to guarantee that a test is waiting for a firestore document to be updated.
Seed utilities
export {
seedDepots,
seedMembers,
seedPatches,
seedPlan,
seedPlanDrivers,
seedRoute,
seedStops,
seedTeam,
seedUsers,
} from '@getcircuit/firestore-test'
Each seeding utility accepts a DeepPartial
version of the data being seeded. Properties that are not explicitly set will fallback to what has been defined inside the src/firebase/data-factories
directory.
Each seeded model may or may not receive an id
property. If no id
is provided, a random one will be generated.
await seedStops({
collection,
stops: [
{ id: 'stop-1', name: 'Stop 1' },
{ name: 'Stop 2' },
],
})
While there are individual methods to seed users, members, depots, etc, you don't need to use them directly. For example, to seed a team with members and their users, you can use _only_ the seedTeam
method:
import { seedTeam } from '@getcircuit/firestore-test'
const teamSelectors = await seedTeam({
members: [
{ id: 'john', name: 'John Doe' },
{ id: 'jane', name: 'Jane Doe' },
],
})
This will automatically create the users for the specified members, and link their DocumentReferences
accordingly.
We can then select these references with the help of the teamSelectors
object:
teamSelectors.membersRef()
teamSelectors.memberRef('john')
teamSelectors.userRef('john')
About seeding users via members
Since each test is expected to seed its own data, the user
collection is shared between all tests. Creating an user via their member is done by using the same id given to the member. So if two different tests created a member with id john
, both tests would try to write data to the same firestore document.
To prevent this, the seedTeam
function prefixes the user id with the id of the team.
const teamSelectors = await seedTeam({
id: 'team-1',
members: [
{ id: 'john', name: 'John Doe' },
{ id: 'jane', name: 'Jane Doe' },
],
})
teamSelectors.userRef('john').id
In case one wants to verify the id of a user, the teamSelectors
object provides a userId
method that will return the prefixed id:
teamSelectors.userId('john')
This is one of the reasons the returned selectors are particularly useful, as this prefixing logic is encapsulated inside them.
About seeding documents with cross-references
As we saw, some of the seeding methods allow you to seed various firestore models at once. While this can speed up writing tests, it can also lead to some confusion when it comes to cross-references.
For example, imagine that we want to seed a team with one member that is assigned to a depot. At the moment of calling seedTeam
, nothing exists, so there's no way to link the member-to-be to the depot-to-be.
const teamSelectors = await seedTeam({
depots: [{ id: 'depot-1' }],
members: [
{
id: 'john',
name: 'John Doe',
depot: ???,
},
],
})
There are two ways to solve this:
- Use the deferred argument of the seeding methods
This is the most concise way, as it allows you to, instead of providing the seeding arguments directly, pass a function that will be called with the selectors. This function should return the data that will be used to seed the model.
const teamSelectors = await seedTeam((teamSelectors) => ({
depots: [{ id: 'depot-1' }],
members: [
{
id: 'john',
name: 'John Doe',
depot: teamSelectors.depotRef('depot-1'),
},
],
}))
- Use the seeding methods separately
This is the most verbose way, but it allows you to have full control over the references. However, you will need to manually link the references and deal with the "user id" conflict mentioned on the About seeding users via members
section.
const teamSelectors = await seedTeam({
depots: [{ id: 'depot-1' }],
})
const userSelectors = await seedUsers({
teamRef: teamSelectors.teamRef,
users: [
{
id: 'john',
name: 'John Doe',
},
],
mapId: (id) => teamSelectors.userId(id),
})
const memberSelectors = await seedMembers({
teamRef: teamSelectors.teamRef,
members: [
{
id: 'john',
name: 'John Doe',
depot: teamSelectors.depotRef('depot-1'),
},
],
})
Writing a test
Each test that connects to the firestore emulator is expected to be self-contained. This means that each test should seed its own data. Cleaning up is not necessary.
import { seedTeam, seedRoute } from '@getcircuit/firestore-test'
test('route is created', async () => {
const { teamRef, memberRef, userRef } = await seedTeam({
members: [{ id: 'chris' }],
})
const { routeRef } = await seedRoute(teamRef, {
route: {
driver: userRef('chris'),
member: memberRef('chris'),
},
})
expect('...').toEqual('...')
})
Configuring the test environment
Setup the test firebase app
The first thing to do is to create a setup file for jest and reference it in the jest.config
setupFilesAfterEnv
property. The first expression in this file must be an import expression for either:
@getcircuit/firestore-test/setup/browser
if you want the test firebase app to use the browser SDK.@getcircuit/firestore-test/setup/node
if you want the test firebase app to use the node SDK.
The import will take care of creating a test firebase application and exposing it via getters. You can then use getTestFirestore
, getTestFirebaseApp
and getTestFirebaseAuth
.
import '@getcircuit/firestore-test/setup/browser'
import { getTestFirestore } from '@getcircuit/firestore-test'
getTestFirestore()
⚠️ If you want to use the node SDK, be sure to follow the firebase-types
package's instructions on how to override the firebase types, as the default types for references, snapshots, etc, are associated with the browser SDK.
Jest config
@getcircuit/firestore-test
provides a JestConfig file to serve as a base for your project's jest
setup:
@getcircuit/firestore-test/jest/config.browser
for a browser environment@getcircuit/firestore-test/jest/config.node
for a node environment
const shared = require('@getcircuit/firestore-test/jest/config.browser')
module.exports = {
...shared,
}
Conflict-free firestore root
In case your Firestore-related test doesn't need a whole team, you can use the getUniqueTestRootRef
method to get a unique document ref pointing to test-suite/{randomId}
. This is useful to avoid having to create a unique collection in each test to prevent conflicts.
import { getUniqueTestRootRef } from '@getcircuit/firestore-test'
test('test 1', async () => {
const rootRef = getUniqueTestRootRef()
const patches = rootRef.collection('patches')
})
test('test 2', async () => {
const rootRef = getUniqueTestRootRef()
const patches = rootRef.collection('patches')
})