Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flakey tests when testing firebase functions using Jest

I'm testing Firebase functions using Jest and the emulator, though the tests are flakey presumably from a race condition. By flakey, I mean sometimes they pass and sometimes they don't, even on the same machine.

Tests and functions are written in TypeScript, then transpiled with babel.

Example test/function

Note: This is an example of just one of the flakey tests. Many other tests are flakey. A solution is preferably one that doesn't just solve this one case, but rather the general issue.

The test

import { onProfilesWrite } from '../src/profiles/on-write'
import { initializeAdminApp } from '@firebase/rules-unit-testing'

const admin = initializeAdminApp({ projectId: 'projectId' }).firestore()

const wrappedFunction = testEnvironment.wrap(onProfilesWrite)

const profilePath = `profiles/${uid}`

const customerProfile = {
    roles: ['customer'],
    slug: 'slug',
    image: 'image.png',
    fullName: 'John Smith',
}

const publisherRoles = ['customer', 'publisher']

const publisherProfile = {
    ...customerProfile,
    roles: publisherRoles,
}

const createChange = async (
    before: Record<string, unknown> | undefined,
    changes: Record<string, unknown>
) => {
    const publisherStatsRef = admin.doc(profilePath)
    if (before) await publisherStatsRef.set(before)

    const beforeSnapshot = await publisherStatsRef.get()
    await publisherStatsRef.set(changes, { merge: true })

    const afterSnapshot = await publisherStatsRef.get()

    return testEnvironment.makeChange(beforeSnapshot, afterSnapshot)
}

test('If user profile is created as a publisher, publisherDetails is created', async () => {
    const change = await createChange(undefined, publisherProfile)
    await wrappedFunction(change)
    const snapshot = await admin.doc(`profileDetails/${uid}`).get()
    const data = snapshot.data()
    expect(data).toBeTruthy()
    expect(data?.id).toBeTruthy()
    expect(data?.slug).toBe(publisherProfile.slug)
    expect(data?.profileImage).toBe(publisherProfile.image)
    expect(data?.publisherName).toBe(publisherProfile.fullName)
    expect(data?.music).toMatchObject([])
})

Run the test

firebase emulators:exec \"jest functions/__tests__ --detectOpenHandles\" --only firestore

Output

If user profile is created as a publisher, publisherDetails is created

    expect(received).toBeTruthy()

    Received: undefined

      46 |     const snapshot = await admin.doc(`profileDetails/${uid}`).get()
      47 |     const data = snapshot.data()
    > 48 |     expect(data).toBeTruthy()
         |                  ^
      49 |     expect(data?.id).toBeTruthy()
      50 |     expect(data?.slug).toBe(publisherProfile.slug)
      51 |     expect(data?.profileImage).toBe(publisherProfile.image)

The function

import * as functions from 'firebase-functions'

// initializes the admin app, then exports admin.firestore
import { firestore } from '../admin'

const database = firestore()

const createPublisherId = async (): Promise<string> => {
    let id = ''
    const MAX_NUMBER = 1000000
    while (id === '') {
        const temporaryId = String(Math.ceil(Math.random() * MAX_NUMBER))
        const snapshot = await firestore()
            .collection('publisherDetails')
            .where('sku', '==', temporaryId)
            .limit(1)
            .get()
        if (snapshot.empty) id = temporaryId
    }
    return id
}

export const createPublisherDetails = async (
    newData: firestore.DocumentData,
    uid: string
): Promise<void> => {
    const id = await createPublisherId()

    await database.doc(`publisherDetails/${uid}`).set(
        {
            id,
            slug: newData.slug,
            publisherName: newData.fullName,
            profileImage: newData.image,
            music: [],
        },
        { merge: true }
    )
}


export const onProfilesWrite = functions.firestore.document('profiles/{uid}').onWrite(
    async (change): Promise<void> => {
        const { id: uid } = change.after
        const oldData = change.before.data()
        const newData = change.after.data()

        if (
            newData?.roles?.includes('publisher') &&
            (typeof oldData === 'undefined' || !oldData.roles?.includes('publisher'))
        )
            await createPublisherDetails(newData, uid)
    }
)

Debug steps

  • All promises are awaited in the cloud functions (as affirmed by an ESLint rule @typescript-eslint/no-floating-promises)
  • Also converted the tests to Mocha (as suggested by the Firebase docs), same errors
  • Converting async/await in tests to promise.then() syntax

Metadata

  • OS: macOS 11.2, Ubuntu 18.04
  • Jest: 26.6.3
  • Firebase: 8.2.6
  • Firebase tools: 9.3.0

As comments roll in, with either questions or suggestions, I'll continue to update this post.

like image 271
Nick Avatar asked Feb 02 '26 02:02

Nick


1 Answers

Change your test portion to as follows :

test('If user profile is created as a publisher, publisherDetails is created', async () => {
  const change = await createChange(undefined, publisherProfile);
  await wrappedFunction(change);
  const documentObject = await admin.doc(`profileDetails/${uid}`);
  const snapshot = await documentObject.get();
  const data = snapshot.data();
  expect(data).toBeTruthy();
  expect(data?.id).toBeTruthy();
  expect(data?.slug).toBe(publisherProfile.slug);
  expect(data?.profileImage).toBe(publisherProfile.image);
  expect(data?.publisherName).toBe(publisherProfile.fullName);
  expect(data?.music).toMatchObject([]);
});

Reason being that in your test region, your use of await is a bit incorrect (function chaining on an object that is being waited for is a big no-no in the same calling line)

like image 136
Kaustubh J Avatar answered Feb 03 '26 16:02

Kaustubh J