How do you mock Firebase Firestore methods using Jest?
Asked Answered
V

8

33

I have a series of functions, each performing various firestore interactions. How do I use Jest to mock these firestore calls? I would like to avoid using a library.

When I use jest.mock("firebase/app") and jest.mock("firebase/firestore") and other variations, I either get null TypeErrors, or errors indicating I am still referencing the actual import and not the mock: Error: ... make sure you call initializeApp().

For example, a simple function I want to test:

import firebase from "firebase/app";
import "firebase/firestore";

export const setDocData = (id, data) => {
  const newDoc = {
    created: firebase.firestore.FieldValue.serverTimestamp(),
    ...data
  };
  firebase
    .firestore()
    .doc("docs/" + id)
    .set(newDoc);
};

Notice how firebase is imported as usual, then firestore is imported a side effect. Also notice how firestore is called first as a function, then later referenced as a property. I believe this is the source of my trouble.

Vikiviking answered 27/8, 2018 at 17:1 Comment(0)
V
24

Here is the solution I have found. There isn't much information online about this, so I hope it helps someone.

EDIT: I believe you can do something similar using jests /__MOCKS__/ folders system, rather than overwriting the firestore object as I do in this example.

The trick is to create a chained API of mock functions, and set this on the firebase object, instead of importing and mocking firestore. The example below allows me to test the above example function, and also doc().get() promises.

const docData = { data: "MOCK_DATA" };
const docResult = {
  // simulate firestore get doc.data() function
  data: () => docData
};
const get = jest.fn(() => Promise.resolve(docResult));
const set = jest.fn();
const doc = jest.fn(() => {
  return {
    set,
    get
  };
});
const firestore = () => {
  return { doc };
};
firestore.FieldValue = {
  serverTimestamp: () => {
    return "MOCK_TIME";
  }
};

export { firestore };

I declare it in a file that runs before all my tests do (see docs), and import and use it in my test files like this:

import firebase from "firebase/app";
import { firestore } from "../setupTests";
firebase.firestore = firestore;

describe("setDocData", () => {
  const mockData = { fake: "data" };
  beforeEach(() => {
    jest.clearAllMocks();
    setDocData("fakeDocID", mockData);
  });

  it("writes the correct doc", () => {
    expect(firestore().doc).toHaveBeenCalledWith("docs/fakeDocID");
  });

  it("adds a timestamp, and writes it to the doc", () => {
    expect(firestore().doc().set).toHaveBeenCalledWith({
      created: "MOCK_TIME",
      fake: "data"
    });
  });
});
Vikiviking answered 27/8, 2018 at 17:1 Comment(0)
V
18

If mocking seems tedious, don't. Use the emulators.

I believe this is a relatively new option for handling reads & writes in testing, so I'm posting it. Here's a quick walk-through.

  1. Download firebase CLI tools.
$ curl -sL firebase.tools | bash
  1. Initialize firebase in your project if you haven't already. Just choose firestore to get started unless you know you'll need the others.
$ firebase init
  1. Configure your firestore instance to point to the emulators (you should be able to mock db for one that redirects to the emulator, but this way will allow you to read/write to the emulator in your development environment also).
const db = firebase.initializeApp(config).firestore()
if (location.hostname === "localhost") {
  db.settings({
    host: "localhost:8080",
    ssl: false
  });
}
  1. Start the emulator. There's also a command that runs the emulators for the duration of a shell command, which you can add to your test suite npm script if you'd like.
$ firebase emulators:start
  1. Test something that uses firestore.
  describe('New city', () => {
    it('should create a new city in firestore', async () => {
      await db.collection('cities').doc('Seattle').set({ state: "WA" })
      const city = await db.collection('cities').doc("Seattle").get()

      expect(city.data()['population']).toEqual("WA")
    })
  })
  1. Optional: Create a db cleanup function that uses the emulator's rest endpoint to remove data between tests.
async function cleanFirestore() {
  const Http = new XMLHttpRequest();
  const url = "http://localhost:8080/emulator/v1/projects/<YOUR-PROJECT-ID>/databases/(default)/documents"

  Http.open("DELETE", url);
  Http.send();

  return new Promise((resolve, reject) => {
    setTimeout(reject, 2000)
    Http.onreadystatechange = resolve
  })
}

For an emulator walkthrough guide from Google: https://google.dev/pathways/firebase-emulators

Docs: https://firebase.google.com/docs/emulator-suite

Vie answered 14/5, 2020 at 14:46 Comment(3)
A good way for setting up integration tests. But mocking is essential if we want to do unit testing.Dysgenics
Agreed. Using the emulator will have a detrimental speed impact.Knox
This would be THE solution, however, it does not work with frontend component tests, I'm afraid. An error is thrown when trying to test a component (INTERNAL ASSERTION FAILED: Unexpected state) which forces you to change the Jest environment to node which leads to another error (ReferenceError: window is not defined) which is understandable as we are trying to test a component on a browserish environment, node is not what we want in this case.Patchouli
G
14

It's been a while since any activity on this question, but still there's not much material online, here's my solution:

export default class FirestoreMock {
  constructor () {
    // mocked methods that return the class
    this.mockCollection = jest.fn(() => this)
    this.mockWhere = jest.fn(() => this)
    this.mockOrderBy = jest.fn(() => this)

    // methods that return promises
    this.mockAdd = jest.fn(() => Promise.resolve(this._mockAddReturn))
    this.mockGet = jest.fn(() => Promise.resolve(this._mockGetReturn))

    // methods that accepts callbacks
    this.mockOnSnaptshot = jest.fn((success, error) => success(this._mockOnSnaptshotSuccess))

    // return values
    this._mockAddReturn = null
    this._mockGetReturn = null
    this._mockOnSnaptshotSuccess = null
  }

  collection (c) {
    return this.mockCollection(c)
  }

  where (...args) {
    return this.mockWhere(...args)
  }

  orderBy (...args) {
    return this.mockOrderBy(...args)
  }

  add (a) {
    return this.mockAdd(a)
  }

  get () {
    return this.mockGet()
  }

  onSnapshot (success, error) {
    return this.mockOnSnaptshot(success, error)
  }

  set mockAddReturn (val) {
    this._mockAddReturn = val
  }

  set mockGetReturn (val) {
    this._mockGetReturn = val
  }

  set mockOnSnaptshotSuccess (val) {
    this._mockOnSnaptshotSuccess = val
  }

  reset () {
    // reset all the mocked returns
    this._mockAddReturn = null
    this._mockGetReturn = null
    this._mockOnSnaptshotSuccess = null

    // reset all the mocked functions
    this.mockCollection.mockClear()
    this.mockWhere.mockClear()
    this.mockOrderBy.mockClear()
    this.mockAdd.mockClear()
    this.mockGet.mockClear()
  }
}

And here's an example usage:

import FirestoreMock from '../test_helpers/firestore.mock'
import firebase from 'firebase/app'
import 'firebase/firestore'

describe('The Agreement model', () => {
    const firestoreMock = new FirestoreMock()
    beforeEach(() => {
        firebase.firestore = firestoreMock
        firestoreMock.reset()
    })

    it('does something', (done) => {
        firestoreMock.mockAddReturn = { id: 'test-id' }
        firebase.firestore.collection('foobar')
          .add({foo: 'bar'})
          .then(res => {
            expect(firestoreMock.mockCollection).toBeCalledWith('foobar')
            expect(firestoreMock.mockAdd).toBeCalledWith({foo: 'bar'})
            expect(res.id).toEqual('test-id')
            done()
          })
          .catch(done)
    })
})

If there is any interest out there I'm fine with packaging the FirestoreMock implementation so that it can be easily shared

Teo

Goodsized answered 4/2, 2020 at 2:53 Comment(0)
C
6

Here is how i mocked firebase for jest.

'use strict'

const collection = jest.fn(() => {
  return {
    doc: jest.fn(() => {
      return {
        collection: collection,
        update: jest.fn(() => Promise.resolve(true)),
        onSnapshot: jest.fn(() => Promise.resolve(true)),
        get: jest.fn(() => Promise.resolve(true))
      }
    }),
    where: jest.fn(() => {
      return {
        get: jest.fn(() => Promise.resolve(true)),
        onSnapshot: jest.fn(() => Promise.resolve(true)),
      }
    })
  }
});

const Firestore = () => {
  return {
    collection
  }
}

Firestore.FieldValue = {
  serverTimestamp: jest.fn()
}

export default class RNFirebase {

  static initializeApp = jest.fn();

  static auth = jest.fn(() => {
    return {
      createUserAndRetrieveDataWithEmailAndPassword: jest.fn(() => Promise.resolve(true)),
      sendPasswordResetEmail: jest.fn(() => Promise.resolve(true)),
      signInAndRetrieveDataWithEmailAndPassword: jest.fn(() => Promise.resolve(true)),
      fetchSignInMethodsForEmail: jest.fn(() => Promise.resolve(true)),
      signOut: jest.fn(() => Promise.resolve(true)),
      onAuthStateChanged: jest.fn(),
      currentUser: {
        sendEmailVerification: jest.fn(() => Promise.resolve(true))
      }
    }
  });

  static firestore = Firestore;

  static notifications = jest.fn(() => {
    return {
        onNotification: jest.fn(),
        onNotificationDisplayed: jest.fn(),
        onNotificationOpened: jest.fn()
    }
  });

  static messaging = jest.fn(() => {
    return {
        hasPermission: jest.fn(() => Promise.resolve(true)),
        subscribeToTopic: jest.fn(),
        unsubscribeFromTopic: jest.fn(),
        requestPermission: jest.fn(() => Promise.resolve(true)),
        getToken: jest.fn(() => Promise.resolve('RN-Firebase-Token'))
    }
  });

  static storage = jest.fn(() => {
    return {
      ref: jest.fn(() => {
        return {
          child: jest.fn(() => {
            return {
              put: jest.fn(() => Promise.resolve(true))
            }
          })
        }
      })
    }
  })

}
Crumpler answered 30/8, 2018 at 15:12 Comment(3)
Thanks, how can I check if for example in firestore collection retrieved with specific where conditionEllingston
something like this expect(collection.where).toHaveBeenCalledWith('assignedNumbers.123', '==', true); I meanEllingston
how does one implement this?Unmeasured
S
5

I used the dependancy injection approach on components and it meant I could mock and test methods without all the boilerplate.

For example, I have a form component that handles invites like so:

import React, { useEffect } from 'react';
import { Formik } from 'formik';
import { validations } from '../../helpers';
import { checkIfTeamExists } from '../helpers';

const Invite = ({ send, userEmail, handleTeamCreation, auth, db, dbWhere }) => {
  useEffect(() => {
    checkIfTeamExists(send, dbWhere);
  }, []);
  return (
      <Formik
        initialValues={{ email: '' }}
        onSubmit={values =>
          handleTeamCreation(userEmail, values.email, db, auth, send)
        }
        validate={validations}
        render={props => (
          <form onSubmit={props.handleSubmit} data-testid="form">
            <input
              type="email"
              placeholder="Please enter your email."
              onChange={props.handleChange}
              onBlur={props.handleBlur}
              value={props.values.email}
              name="email"
            />
            {props.errors.email && (
              <p className="red" data-testid="error">
                {props.errors.email}
              </p>
            )}
            <button type="submit">Submit</button>
          </form>
        )}
      />
  );
};

export default Invite;

The checkIfTeamExists method relies on firebase auth and handleTeamCreation method writes to the firestore.

When I referenced the component in its parent I instantiated it like so:

<Invite
 send={send}
 userEmail={value.user.user.email}
 handleTeamCreation={handleTeamCreation}
 auth={auth.sendSignInLinkToEmail}
 db={db.collection('games')}
 dbWhere={db.collection('games')
            .where('player1', '==', value.user.user.email)
            .get}
 />

Then, using react-testing-library, in my tests I was able to mock things out with a simple jest.fn().

test('Invite form fires the send function on Submit ', async () => {
  const handleTeamCreation = jest.fn();
  const send = jest.fn();
  const userEmail = '[email protected]';
  const db = jest.fn();
  const auth = jest.fn();
  const dbWhere = jest.fn().mockResolvedValue([]);
  const { getByPlaceholderText, getByTestId } = render(
    <Invite
      send={send}
      userEmail={userEmail}
      handleTeamCreation={handleTeamCreation}
      auth={auth}
      db={db}
      dbWhere={dbWhere}
    />
  );
  const inputNode = getByPlaceholderText('Please enter your email.');
  const email = '[email protected]';
  fireEvent.change(inputNode, { target: { value: email } });
  const formNode = getByTestId('form');
  fireEvent.submit(formNode);
  await wait(() => {
    expect(handleTeamCreation).toHaveBeenCalledWith(
      userEmail,
      email,
      db,
      auth,
      send
    );

    expect(handleTeamCreation).toHaveBeenCalledTimes(1);
  });
});

and mocked the firestore where query in the same way.

test('Invite form must contain a valid email address', async () => {
  const send = jest.fn();
  const db = jest.fn();
  const dbWhere = jest.fn().mockResolvedValue([]);

  const { getByPlaceholderText, queryByTestId } = render(
    <Invite send={send} db={db} dbWhere={dbWhere} />
  );
  expect(queryByTestId('error')).not.toBeInTheDocument();
  const inputNode = getByPlaceholderText('Please enter your email.');
  const email = 'x';
  fireEvent.change(inputNode, { target: { value: email } });

  await wait(() => {
    expect(queryByTestId('error')).toHaveTextContent('Invalid email address');
  });
});

This is very simple, but it works. It's also quite verbose but I thought a real use case would be more helpful than a contrived example. I hope this helps someone.

Smoodge answered 10/12, 2018 at 8:44 Comment(0)
L
2

I found that mocking the import works well. I added this code in the test, above where I render my component importing 'firebase/app'

jest.mock('firebase/app', () => ({
  __esModule: true,
  default: {
    apps: [],
    initializeApp: () => {},
    auth: () => {},
  },
}));
Lambskin answered 17/12, 2019 at 21:29 Comment(1)
Could you add some examples that how it is tested?Outbalance
C
1

I am using firebase.firestore.FieldValue.serverTimestamp() in one of my functions:

import firestore from '@react-native-firebase/firestore';
import firebase from  '@react-native-firebase/app';

export function getUserCreateObject() {
    return {
        property1: {
          property2: {
            value1: true,
            last_updated_date: firebase.firestore.FieldValue.serverTimestamp(),
          },
          //the rest of the JSON object
        },
      };
}

To mock this, I have a jest.setup.js file which I reference in my package.json:

"jest": {
    "preset": "react-native",
    "moduleDirectories": [
      "node_modules",
      "src"
    ],
    "transform": {
      "\\.js$": "<rootDir>/node_modules/babel-jest"
    },
    "transformIgnorePatterns": [
      "/node_modules/(?!(jest-)?react-native|@react-native-firebase/auth|@react-native-firebase/app|@react-native-firebase/app-types)"
    ],
    "setupFiles": [
      "./jest/jest.setup.js"
    ],
    "coveragePathIgnorePatterns": [
      "/node_modules/",
      "/jest"
    ]
  }

And in jest.setup.js I do:

jest.mock('@react-native-firebase/app', () => ({
    firestore: {
      FieldValue: {
        serverTimestamp: jest.fn(),
      }
    }
}));
Cohosh answered 7/4, 2020 at 8:38 Comment(0)
M
-1
//firebase mock
import * as messaging from '@react-native-firebase/messaging';

jest.spyOn(messaging, 'default').mockImplementation(() => ({
  onMessage: () => {},
  getToken: () => {},
  onTokenRefresh: () => {},
  subscribeToTopic: jest.fn(),
  unsubscribeFromTopic: jest.fn(),
}));
Microvolt answered 6/4, 2022 at 10:33 Comment(1)
This is a quite simple mock for messaging. However OP asked about firestore.Uboat

© 2022 - 2025 — McMap. All rights reserved.