Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@getcircuit/firestore-test

Package Overview
Dependencies
Maintainers
19
Versions
186
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@getcircuit/firestore-test

<!-- @import "[TOC]" {cmd="toc" depthFrom=2 depthTo=4 orderedList=false} -->

  • 15.17.1
  • unpublished
  • npm
  • Socket score

Version published
Weekly downloads
0
Maintainers
19
Weekly downloads
 
Created
Source

@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.

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.

# Start the emulator indefinitely
firestore-test emulator:start

# Start the emulator and run a command
firestore-test emulator:exec "<command>"

Example:

# Start the emulator and run jest
firestore-test emulator:exec "jest"

Configuring the Test Environment

Setting Up the Test Firebase App

To start, create a Jest setup file and reference it in the setupFilesAfterEnv property within your jest.config.js. The first line in this setup file should import one of the following:

  • @getcircuit/firestore-test/setup/client if you're running tests in a client environment.
  • @getcircuit/firestore-test/setup/admin if you're running tests in a admin environment.
// jest.config.js
export default {
  setupFilesAfterEnv: ['@getcircuit/firestore-test/setup/client'],
}

The test firebase app is automatically initialized once getTestFirestore, getTestAuth or getTestApp is called.

⚠️ Note: If you opt for the Node SDK, make sure to follow the instructions in the firebase-types package to override the default Firebase types. This is essential as the default types are set for the browser SDK.

For more information about how we support both firebase-admin and firebase in the same project, check out this document.

Configuring Jest

@getcircuit/firestore-test provides pre-configured Jest setup files for both browser and Node.js environments. You can extend these in your project's Jest configuration:

  • @getcircuit/firestore-test/jest/config.browser for a browser environment
  • @getcircuit/firestore-test/jest/config.node for a Node.js environment

When integrating these configurations, make sure to merge the globalSetup and setupFilesAfterEnv fields to avoid overwriting any existing configurations in your project's Jest setup.

// client-project/jest.config.js
const sharedConfig = require('@getcircuit/firestore-test/jest/config.browser')

module.exports = {
  ...sharedConfig,
  globalSetup: [
    ...(sharedConfig?.globalSetup ?? []),
    // your globalSetup here
  ],
  setupFilesAfterEnv: [
    ...(sharedConfig?.setupFilesAfterEnv ?? []),
    // your setupFilesAfterEnv here
  ],
  // Additional custom Jest configurations
}

Seed utilities

Seeding methods

This package offers three distinct types of utilities to seed data into Firestore, each serving a different purpose and level of complexity:

  • seed{Model} methods: These methods are your starting point for data initialization. They allow you to create just the basic Firestore document for a given model—no more, no less. Ideal for setting up minimal, isolated test scenarios or manually constructing a non-standard data setup (i.e member with broken user references, etc).

  • populate{Model} methods: Once you've sown your "seed" with the basic document, you can use these methods to grow it by adding related data. These are particularly useful when you need to build upon a basic setup incrementally. For example, you can populate a team with members and depots after a seedTeam.

  • bootstrap{Model} methods: These utilities are designed for comprehensive data setup in a single step, ensuring that all relationships between the data entities are correctly configured. Use bootstrap when you need a fully-integrated, relationally coherent data set.

Available seeding methods:

import {
  // Team
  seedTeam, bootstrapTeam, populateTeam,
  // User
  seedUser, seedUsers, bootstrapUser, bootstrapUsers,
  // Member
  seedMember, seedMembers, bootstrapMember, bootstrapMembers,
  // Plan
  seedPlan, bootstrapPlan, populatePlan, seedPlanDriver, seedPlanDrivers,
  // Route
  seedRoute, seedRoutes, bootstrapRoute, populateRoute,
  // Depot
  seedDepot, seedDepots,
  // Recipient
  seedRecipient, seedRecipients, populateRecipient, bootstrapRecipient,
  // Stop
  seedStop, seedStopOnRoot, seedStops, seedStopsOnRoot,
  // Package
  seedPackages, seedPackagesOnRecipient,
  // Products
  seedProducts,
  // Patches
  seedPatches,
  // Experiments
  seedExperiment, seedExperiments,
  // DriverZone
  seedDriverZone, seedDriverZones,
  // Breaks
  seedBreaks,
  // LocationLogs
  seedLocationLogs,
  // Shopify Sessions
  seedShopifySession, seedShopifySessions
} from '@getcircuit/firestore-test';
When to Use Which?

Understanding which utility to use depends on the testing scenario you're facing:

  • Standard, Interconnected Data: If you're looking to simulate a real-world, production-like environment where all data entities maintain standard and correct relationships, bootstrap{Model} methods are your go-to. They provide a comprehensive, functional data structure out of the box.

  • Edge Cases and Anomalies: When your tests require unconventional or incorrect data relationships—where you need to break the standard links between entities for testing purposes—opt for the seed{Model} methods. These give you the flexibility to manually construct specific scenarios with granular control over data relationships.

  • Partial Data Extension: If you've already set up some basic data structures using seed{Model} and want to extend them by adding related data entities, consider using populate{Model} methods. These are ideal for incrementally building upon an existing setup.

By choosing the appropriate utility, you ensure that your tests are both effective and efficient, whether you're testing core functionalities or edge cases.

Seed Recipes

Even with the flexibility offered by seed, populate, and bootstrap, there are unique test scenarios that these utilities might not cover completely. This is where "Seed Recipes" come into play.

Recipes are normal functions prefixed by prepare that bootstraps/seeds multiple documents into the database for a given context. In contrast to the seeding utilities provided by this package, recipes may have a loose API and receive or return whatever fits the use case.

They are designed for particular test scenarios that don't fit the one-size-fits-all approach of general-purpose seeding utilities. For example, creating a team with a specific number of members, or a route with a specific number of stops, or create a team/user/route in one go.

Structuring Seed Recipes

We recommend organizing your Seed Recipes in a seed_recipes folder within your project. Each module in this folder should contain one recipe function, prefixed by prepare. All the recipes should then be re-exported by the index file of the seed_recipes folder.

Here's a simple example:

// In seed_recipes/prepareDriverWithRoute.ts

/**
 * Bootstraps a team with a driver member and assign them to the defined route.
 */
export async function prepareDriverWithRoute(/* ... */) {
  // ...
}

// In seed_recipes/index.ts
export * from './prepareDriverWithRoute'

By adhering to these conventions and using Seed Recipes wisely, you can ensure that your Firestore tests are both effective and flexible, capable of handling both general use-cases and unique edge cases.

firebase-admin exclusive utilities

The seeding utilities are designed to work in both the browser and node environments. However, when in the context of a node environment (firebase-admin) any bootstrapped user will also have their Auth account created. This is not the case in the browser environment (firebase), as the browser SDK doesn't allow for creating users directly.

The following utilities are only available in the firebase-admin environment:

  • createIdTokenForEmailAndPassword - Creates an id token for the given email and password.
  • createIdTokenForUID - Creates an id token for the given uid.
  • seedAccount - Seeds an account with the given data.
  • findAccount - Finds an account with the given email or phone number.

Using the seeding utilities

Customizing data

Each utility allows for partial data input through a DeepPartial of the model. This enables you to specify only the data you need for a test, with any unspecified data falling back to defaults set in src/firebase/data-factories.

For example, 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, [
  { id: 'stop-1', name: 'Stop 1' }, // Stop 1 will have a known id
  { name: 'Stop 2' }, // Stop 2 will have a random id
])

While there are individual methods to seed users, members, depots, etc, you don't need to use them directly as explained in the when to use which section. Instead, you can use the populateTeam or boostrapTeam method to seed a team with all its related data.

For example, to seed a team with members and their respective users:

import {
  seedTeam,
  populateTeam,
  bootstrapTeam,
} from '@getcircuit/firestore-test'

// Using both seedTeam and populateTeam
const teamSelectors1 = await seedTeam()
await populateTeam(teamSelectors1.teamRef, {
  members: [
    { id: 'john', name: 'John Doe' },
    { id: 'jane', name: 'Jane Doe' },
  ],
})

// Using bootstrapTeam
const teamSelectors2 = await bootstrapTeam(
  {},
  {
    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() // => CollectionReference<Member>
teamSelectors.memberRef('john') // => DocumentReference<Member>
teamSelectors.userRef('john') // => DocumentReference<User>

Handling Shared user IDs Across Tests

In a testing environment where each test is expected to be isolated and seed its own data, the user collection is shared among all tests. Importantly, there is no expectation to reset any Firestore data or collections between tests. This presents a risk of ID conflicts when multiple tests attempt to create a user with the same ID via their associated members.

To address this issue, the utilities handle IDs in a specific manner depending on the context:

  • Direct User Seeding: Using seedUser or seedUsers directly will not involve any automatic ID prefixing. What you specify is what you get.

  • Seeding via member: When a user is created as part of member seeding through bootstrapTeam or populateTeam, the ID for the user is automatically prefixed with the ID of the team to which the member belongs. This ensures uniqueness across tests.

test('some test', async () => {
  const teamSelectors = await bootstrapTeam(
    { id: 'team-1' },
    { members: [{ id: 'john', name: 'John Doe' }] },
  )

  // Member id can be `john`, as the member collection is isolated to the team
  expect(teamSelectors.memberRef('john').id).toBe('john')

  // User id is prefixed with the team id to avoid conflicts as the user collection is shared
  expect(teamSelectors.userRef('john').id).toBe('team-1-john')
})

test('some other test', async () => {
  const teamSelectors = await bootstrapTeam(
    { id: 'team-2' },
    { members: [{ id: 'john', name: 'John Doe' }] },
  )

  // Member id can be `john`, as the member collection is isolated to the team
  expect(teamSelectors.memberRef('john').id).toBe('john')

  // User id is prefixed with the team id to avoid conflicts as the user collection is shared
  expect(teamSelectors.userRef('john').id).toBe('team-2-john')
})

If you need to reference this prefixed user ID, the returned teamSelectors object provides a userId method for convenience:

teamSelectors.userId('john') // => 'team-1-john'

This feature ensures that tests remain isolated in their data setup, thereby making the utilities robust and easy to use.

Handling Cross-References in Seeded Data

Creating cross-references between Firestore documents can be tricky, especially when the documents are being seeded simultaneously. For example, how can you link a member to a depot when both are just about to be created? This challenge arises often when using, for example, populateTeam or bootstrapTeam.

There are two main approaches to handle this:

1. Using a Function with populate or bootstrap

You can use a deferred argument function to dynamically generate the seeding data. The function receives the selectors as an argument, allowing you to reference previously created documents.

const teamSelectors = await bootstrapTeam({}, (selectors) => ({
  depots: [{ id: 'depot-1' }],
  members: [
    {
      id: 'john',
      name: 'John Doe',
      depot: selectors.depotRef('depot-1'), // Linking made easy
    },
  ],
}))
2. Using Seeding Methods Separately

For more granular control, you can use the seeding methods individually. This approach requires manually linking the references.

// First, seed the team
const teamSelectors = await seedTeam()

// Then, seed the depot
const { depotRef } = await seedDepot(teamSelectors.teamRef, {
  id: 'depot-1',
  name: 'Depot 1',
})

// Finally, bootstrap the member and link them to the depot
await bootstrapMember(teamSelectors.teamRef, {
  id: 'john',
  name: 'John Doe',
  depot: depotRef,
})

By choosing the appropriate method, you can effectively manage the complexities of document cross-referencing in your Firestore tests.

Other testing utilities

export {
  getTestFirestore,
  getTestRefSelectors,
  getUniqueTestRootRef,
  waitForSnapshotListenersInSync,
  fakeId,
  fakeEmail,
  fakePhoneNumber,
  fakeShopifySessionId,
  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.

  • waitForSnapshotListenersInSync() 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.

  • fakeId() returns a random id that can be used to seed data.

  • fakeEmail() returns a random email that can be used to seed data.

  • fakePhoneNumber() returns a random phone number that can be used to seed data.

  • fakeShopifySessionId() returns a random shopify session id that can be used to seed data.

  • FIREBASE_TEST_PROJECT_ID is the id of the test project. It's _firebase_test_ currently.

  • 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.

Writing tests

Test isolation

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 expected nor necessary.

import { bootstrapTeam, bootstrapRoute } from '@getcircuit/firestore-test'

test('route is created', async () => {
  // Bootstrap a team with all its related data, including members and users
  const teamSelectors = await bootstrapTeam(
    {},
    {
      members: [{ id: 'chris', name: 'Chris Doe' }],
    },
  )

  // Bootstrap a route while linking it to the driver user and member documents
  const routeSelectors = await bootstrapRoute(teamSelectors.teamRef, {
    route: {
      driver: teamSelectors.userRef('chris'),
      member: teamSelectors.memberRef('chris'),
    },
  })

  // Test assertions
  expect('...').toEqual('...')
})

Conflict-free Firestore Root

When your Firestore-related test doesn't require a complete team setup, you can achieve test isolation by using a unique Firestore root for each test. The getUniqueTestRootRef function provides a convenient way to do this. This function returns a unique document reference pointing to test-suite/{randomId}, effectively isolating each test's data.

By using this approach, you can prevent conflicts when running multiple tests that may otherwise interact with the same Firestore collection names.

import { getUniqueTestRootRef } from '@getcircuit/firestore-test'

test('test 1', async () => {
  const rootRef = getUniqueTestRootRef()
  const patches = rootRef.collection('patches')

  // Perform operations on this patches collection, isolated from other tests
})

test('test 2', async () => {
  const rootRef = getUniqueTestRootRef() // Different from the rootRef in the previous test
  const patches = rootRef.collection('patches') // Despite having the same collection name, these are isolated instances

  // Continue with test-specific operations on this isolated patches collection
})

By using getUniqueTestRootRef, you ensure that each test has its own unique path in Firestore, allowing for conflict-free and independent test execution.

FAQs

Package last updated on 15 Jan 2024

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc