firestore-vitest-mock
Version:
Vitest helper for mocking Google Cloud Firestore
490 lines (416 loc) • 15.3 kB
text/typescript
import { beforeEach, afterEach, describe, expect, vi, test } from "vitest";
import { mockFirebase, mockInitializeApp } from "../mocks/firebase";
import {
mockSettings,
mockUseEmulator,
mockAdd,
mockCollectionGroup,
mockSet,
mockBatch,
mockBatchCommit,
mockBatchDelete,
mockBatchSet,
mockBatchUpdate,
mockUpdate,
mockOnSnapShot,
CollectionReference,
DocumentReference,
mockCollection,
mockDoc,
} from "../mocks/firestore";
import {
mockSelect,
mockGet,
mockWhere,
mockQueryOnSnapshot,
mockWithConverter,
} from "../mocks/query";
import { mockTimestampNow, Timestamp } from "../mocks/timestamp";
const filters = false;
describe("we can start a firebase application (query filters: false)", () => {
const flushPromises = async (): Promise<void> => {
await new Promise(setImmediate);
};
mockFirebase(
{
database: {
users: [
{ id: "abc123", first: "Bob", last: "builder", born: 1998 },
{
id: "123abc",
first: "Blues",
last: "builder",
born: 1996,
_collections: {
cities: [
{
id: "LA",
name: "Los Angeles",
state: "CA",
country: "USA",
visited: true,
},
{
id: "Mex",
name: "Mexico City",
country: "Mexico",
visited: true,
},
],
},
},
],
cities: [
{ id: "LA", name: "Los Angeles", state: "CA", country: "USA" },
{
id: "DC",
name: "Disctric of Columbia",
state: "DC",
country: "USA",
},
],
},
},
{ simulateQueryFilters: filters }
);
// });
/** @type {import('firebase').default} */
let firebase;
beforeEach(async () => {
firebase = await import("firebase");
vi.resetAllMocks();
firebase.initializeApp({
apiKey: "### FIREBASE API KEY ###",
authDomain: "### FIREBASE AUTH DOMAIN ###",
projectId: "### CLOUD FIRESTORE PROJECT ID ###",
});
});
afterEach(() => {
mockTimestampNow.mockClear();
});
test("We can start an application", async () => {
const db = firebase.firestore();
db.settings({ ignoreUndefinedProperties: true });
expect(mockInitializeApp).toHaveBeenCalled();
expect(mockSettings).toHaveBeenCalledWith({
ignoreUndefinedProperties: true,
});
});
test("we can use emulator", async () => {
const db = firebase.firestore();
db.useEmulator("localhost", 9000);
expect(mockUseEmulator).toHaveBeenCalledWith("localhost", 9000);
});
describe("Examples from documentation", () => {
test("add a user", () => {
const db = firebase.firestore();
// Example from documentation:
// https://firebase.google.com/docs/firestore/quickstart#add_data
return db
.collection("users")
.add({
first: "Ada",
last: "Lovelace",
born: 1815,
})
.then(function (docRef) {
expect(mockAdd).toHaveBeenCalled();
expect(docRef).toHaveProperty("id");
expect(docRef).toHaveProperty("path", `users/${docRef.id}`);
});
});
test("get all users", () => {
const db = firebase.firestore();
// Example from documentation:
// https://firebase.google.com/docs/firestore/quickstart#read_data
return db
.collection("users")
.get()
.then((querySnapshot) => {
expect(querySnapshot.forEach).toBeTruthy();
expect(querySnapshot.docs.length).toBe(2);
expect(querySnapshot.size).toBe(querySnapshot.docs.length);
const paths = querySnapshot.docs.map((d) => d.ref.path).sort();
const expectedPaths = ["users/abc123", "users/123abc"].sort();
expect(paths).toStrictEqual(expectedPaths);
querySnapshot.forEach((doc) => {
expect(doc.exists).toBe(true);
expect(doc.data()).toBeTruthy();
expect(doc.data().id).toBeFalsy();
});
});
});
test("select specific fields only", () => {
const db = firebase.firestore();
// Example from documentation:
// https://firebase.google.com/docs/firestore/quickstart#read_data
return db
.collection("users")
.select("first", "last")
.get()
.then((querySnapshot) => {
expect(mockSelect).toHaveBeenCalledWith("first", "last");
const data = querySnapshot.docs[0].data();
expect(Object.keys(data).length).toBe(2);
expect(data.first).toBe("Bob");
expect(data.last).toBe("builder");
});
});
test("select refs only", () => {
const db = firebase.firestore();
// Example from documentation:
// https://firebase.google.com/docs/firestore/quickstart#read_data
return db
.collection("users")
.select()
.get()
.then((querySnapshot) => {
expect(mockSelect).toHaveBeenCalledWith();
const data = querySnapshot.docs[0].data();
expect(Object.keys(data).length).toBe(0);
});
});
test("collectionGroup with collections only at root", () => {
const db = firebase.firestore();
// Example from documentation:
// https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query
return db
.collectionGroup("users")
.where("last", "==", "builder")
.get()
.then((querySnapshot) => {
expect(mockCollectionGroup).toHaveBeenCalledWith("users");
expect(mockGet).toHaveBeenCalledTimes(1);
expect(mockWhere).toHaveBeenCalledWith("last", "==", "builder");
expect(querySnapshot.forEach).toBeTruthy();
expect(querySnapshot.docs.length).toBe(2);
expect(querySnapshot.size).toBe(querySnapshot.docs.length);
querySnapshot.forEach((doc) => {
expect(doc.exists).toBe(true);
expect(doc.data()).toBeTruthy();
});
});
});
test("collectionGroup with subcollections", () => {
const db = firebase.firestore();
return db
.collectionGroup("cities")
.get()
.then((querySnapshot) => {
expect(mockCollectionGroup).toHaveBeenCalledWith("cities");
expect(mockGet).toHaveBeenCalledTimes(1);
expect(mockWhere).not.toHaveBeenCalled();
expect(querySnapshot.forEach).toBeTruthy();
expect(querySnapshot.docs.length).toBe(4);
expect(querySnapshot.size).toBe(querySnapshot.docs.length);
querySnapshot.forEach((doc) => {
expect(doc.exists).toBe(true);
expect(doc.data()).toBeTruthy();
});
});
});
test("collectionGroup with queried subcollections", () => {
const db = firebase.firestore();
return db
.collectionGroup("cities")
.where("country", "==", "USA")
.get()
.then((querySnapshot) => {
expect(mockCollectionGroup).toHaveBeenCalledWith("cities");
expect(mockGet).toHaveBeenCalledTimes(1);
expect(mockWhere).toHaveBeenCalledWith("country", "==", "USA");
expect(querySnapshot.forEach).toBeTruthy();
expect(querySnapshot.docs.length).toBe(filters ? 3 : 4);
expect(querySnapshot.size).toBe(querySnapshot.docs.length);
querySnapshot.forEach((doc) => {
expect(doc.exists).toBe(true);
expect(doc.data()).toBeTruthy();
});
});
});
test("set a city", () => {
const db = firebase.firestore();
// Example from documentation:
// https://firebase.google.com/docs/firestore/manage-data/add-data#set_a_document\
return db
.collection("cities")
.doc("LA")
.set({
name: "Los Angeles",
state: "CA",
country: "USA",
})
.then(function () {
expect(mockSet).toHaveBeenCalledWith({
name: "Los Angeles",
state: "CA",
country: "USA",
});
});
});
test("updating a city", () => {
const db = firebase.firestore();
// Example from documentation:
// https://firebase.google.com/docs/firestore/manage-data/add-data#update-data
const washingtonRef = db.collection("cities").doc("DC");
const now = Timestamp._fromMillis(new Date().getTime());
mockTimestampNow.mockReturnValue(now);
// Set the "capital" field of the city 'DC'
return washingtonRef
.update({
capital: true,
})
.then(function (value) {
expect(value.updateTime).toStrictEqual(now);
expect(mockUpdate).toHaveBeenCalledWith({ capital: true });
});
});
test("batch writes", () => {
const db = firebase.firestore();
// Example from documentation:
// https://cloud.google.com/firestore/docs/manage-data/transactions
// Get a new write batch
const batch = db.batch();
// Set the value of 'NYC'
const nycRef = db.collection("cities").doc("NYC");
batch.set(nycRef, { name: "New York City" });
// Update the population of 'SF'
const sfRef = db.collection("cities").doc("SF");
batch.update(sfRef, { population: 1000000 });
// Delete the city 'LA'
const laRef = db.collection("cities").doc("LA");
batch.delete(laRef);
// Commit the batch
return batch.commit().then(function (result) {
expect(result).toBeInstanceOf(Array);
expect(mockBatch).toHaveBeenCalled();
expect(mockBatchDelete).toHaveBeenCalledWith(laRef);
expect(mockBatchUpdate).toHaveBeenCalledWith(sfRef, {
population: 1000000,
});
expect(mockBatchSet).toHaveBeenCalledWith(nycRef, {
name: "New York City",
});
expect(mockBatchCommit).toHaveBeenCalled();
});
});
test("listCollections method does not exist", async () => {
const db = firebase.firestore();
expect(() => {
db.collection("cities").doc("LA").listCollections();
}).toThrow(TypeError);
});
test("onSnapshot single doc", async () => {
const db = firebase.firestore();
const now = Timestamp._fromMillis(new Date().getTime());
mockTimestampNow.mockReturnValue(now);
// Example from documentation:
// https://firebase.google.com/docs/firestore/query-data/listen
db.collection("cities")
.doc("LA")
.onSnapshot((doc) => {
expect(doc).toHaveProperty("createTime");
expect(doc).toHaveProperty("data");
expect(doc.data).toBeInstanceOf(Function);
expect(doc).toHaveProperty("metadata");
expect(doc).toHaveProperty("readTime");
expect(doc).toHaveProperty("updateTime");
expect(doc.readTime).toStrictEqual(now);
});
await flushPromises();
expect(mockOnSnapShot).toHaveBeenCalled();
expect(mockQueryOnSnapshot).not.toHaveBeenCalled();
});
test("onSnapshot can work with options", async () => {
const db = firebase.firestore();
const now = Timestamp._fromMillis(new Date().getTime());
mockTimestampNow.mockReturnValue(now);
// Example from documentation:
// https://firebase.google.com/docs/firestore/query-data/listen
db.collection("cities")
.doc("LA")
.onSnapshot(
{
// Listen for document metadata changes
includeMetadataChanges: true,
},
(doc) => {
expect(doc).toHaveProperty("createTime");
expect(doc).toHaveProperty("data");
expect(doc.data).toBeInstanceOf(Function);
expect(doc).toHaveProperty("metadata");
expect(doc).toHaveProperty("readTime");
expect(doc).toHaveProperty("updateTime");
expect(doc.readTime).toStrictEqual(now);
}
);
await flushPromises();
expect(mockOnSnapShot).toHaveBeenCalled();
expect(mockQueryOnSnapshot).not.toHaveBeenCalled();
});
test("onSnapshot with query", async () => {
const db = firebase.firestore();
// Example from documentation:
// https://firebase.google.com/docs/firestore/query-data/listen
const unsubscribe = db
.collection("cities")
.where("state", "==", "CA")
.onSnapshot((querySnapshot) => {
expect(querySnapshot).toHaveProperty("forEach", expect.any(Function));
expect(querySnapshot).toHaveProperty("docChanges");
expect(querySnapshot).toHaveProperty("docs", expect.any(Array));
expect(querySnapshot.forEach).toBeInstanceOf(Function);
expect(querySnapshot.docChanges).toBeInstanceOf(Function);
expect(querySnapshot.docs).toBeInstanceOf(Array);
expect(querySnapshot.docChanges()).toBeInstanceOf(Array);
});
await flushPromises();
expect(unsubscribe).toBeInstanceOf(Function);
expect(mockWhere).toHaveBeenCalled();
expect(mockOnSnapShot).not.toHaveBeenCalled();
expect(mockQueryOnSnapshot).toHaveBeenCalled();
});
describe("withConverter", () => {
const converter = {
fromFirestore: () => {},
toFirestore: () => {},
};
test("single document", async () => {
const db = firebase.firestore();
const recordDoc = db.doc("cities/la").withConverter(converter);
expect(mockDoc).toHaveBeenCalledWith("cities/la");
expect(mockWithConverter).toHaveBeenCalledWith(converter);
expect(recordDoc).toBeInstanceOf(DocumentReference);
const record = await recordDoc.get();
expect(mockGet).toHaveBeenCalled();
expect(record).toHaveProperty("id", "la");
expect(record.data).toBeInstanceOf(Function);
});
test("single undefined document", async () => {
const db = firebase.firestore();
const recordDoc = db
.collection("cities")
.withConverter(converter)
.doc();
expect(mockCollection).toHaveBeenCalledWith("cities");
expect(mockWithConverter).toHaveBeenCalledWith(converter);
expect(mockDoc).toHaveBeenCalled();
expect(recordDoc).toBeInstanceOf(DocumentReference);
const record = await recordDoc.get();
expect(mockGet).toHaveBeenCalled();
expect(record).toHaveProperty("id");
expect(record.data).toBeInstanceOf(Function);
});
test("multiple documents", async () => {
const db = firebase.firestore();
const recordsCol = db.collection("cities").withConverter(converter);
expect(mockCollection).toHaveBeenCalledWith("cities");
expect(mockWithConverter).toHaveBeenCalledWith(converter);
expect(recordsCol).toBeInstanceOf(CollectionReference);
const records = await recordsCol.get();
expect(mockGet).toHaveBeenCalled();
expect(records).toHaveProperty("docs", expect.any(Array));
});
});
});
});