sinon stub a firebase collection with chained methods for unit testing...(sinon, node/express, Jasmine)

Viewed 387

In my nodejs code, I have a snippet:

   await cfDB.collection('users').doc(userAccount.uid).set({
      email: 'email@domain.com',
      firstName: 'firstname',
      lastName: 'Lastname,
    });

How would I create a sinon mock of this for unit test with jasmine?

more complete context....

 const { Firestore } = require('@google-cloud/firestore');

  const cfDB = new Firestore();

  // Create Firestore user document
  try {
    await cfDB.collection('users').doc('E7skP0IncSW7wkBxnrYFx6udzGH2').set({
      email: 'email@domain.com',
      firstName: 'Firstname',
      lastName: 'Lastname',
    });


  } catch (error) {
    res.json({ success: false, payload: { message: 'Firebase User write error. ' + error } });
    return;
  }

  // Success
  res.json({ success: true, payload: { message: 'User created.', userAccount: userAccount } });
}
5 Answers

Here is the solution...

        const collectionStub = sinon.stub(firebase.cfDB, 'collection')
        .get(() => {
            return function() {
                return {
                    doc: (path) => {
                        return {
                            set: () => [{user: 'mock-user-1'}, {user: 'mock-user-2'}]
                        }
                    }
                }
            }
        });

I'm still working on it, but I've been able to get this far for now:

function stubTest(path: string, result: any){
    sinon.stub(admin, 'firestore')
    .get(() => {
        return function() {
            return stubCollection(path, result);            
        }
    });
}
function stubDoc(path: string, result: any){
    const parts = path.split('/');
    const rest = parts.slice(1).join('/');
    return {
        doc: () => {
            if(rest){
                return stubCollection(rest, result);
            }else {
                return {
                    get: () => stubData(result)               
                }                
            }
        }
    }
}
function stubCollection(path: string, result: any){
    const parts = path.split('/');
    const rest = parts.slice(1).join('/');
    return {
        collection: () => {
            if(rest){
                return stubDoc(rest, result);
            }else {
                return {
                    get: () => stubData(result)
                }                
            }
        }
    }
}
function stubData(result: any){
    return {
        data: () => result
    }
}

and to use it you only need to call stubTest("restaurants/12333/orders/123111", {mockResponse: "someData"});

this will mock a call like this one

const order = (await firestore.collection('restaurants').doc('12333').collection('orders').doc('123111').get()).data();

Ofc there is a long way to go, adding more mock methods but at least I dont need to write stubs multiple times

Here is a complete example on how I use sinon to mock away Firestore.

In the function-under-test getAssets I pass firestore as its first argument. You can also choose just replace a global firestore variable using rewire or proxyquire or any other way, that would be out of scope for this answer.

In the test, there is an example AssetData[] that represents a collection. And the call to setupAssets turns this into a mocked firestore that would return this collection when queried.

The function setupAssets returns all the intermediate mocks it creates as well, these can be inspected for their call arguments and number of calls in your test.

Example test with mocked firestore:

it('gets existing assets', async () => {
  const userId = 'user-id'
  const assetName = 'asset-name'
  const assets: AssetData[] = [{ asset: assetName, amount: 1, avgPrice: '1.00' }]
  const { firestore } = setupAssets(assetName, userId, assets)

  const result = await assetFuncs.getAssets(firestore, userId, assetName)
  t.deepEqual(result.docs, assets)
})

The actual code that gets the assets:

async function getAssets (firestore: firestore.Firestore, userId: string, asset: string) {
  const colRef = firestore.collection(`users/${userId}/assets`)
  const query = colRef.where('asset', '==', asset)
  return query.get()
}

And finally the sinon mocking function that sets up a fake Firestore:

function setupAssets (assetName: string, userId: string, assets: AssetData[]) {
  const firestore = sinon.createStubInstance(firestore.Firestore)
  const collectionRef = sinon.createStubInstance(firestore.CollectionReference)
  const query = sinon.createStubInstance(firestore.Query)
  const querySnapshot = sinon.createStubInstance(firestore.QuerySnapshot)
  const docSnapshots = assets.map(asset => {
    const docSnapshot = sinon.createStubInstance(firestore.DocumentSnapshot)
    Object.defineProperty(docSnapshot, 'id', { value: crypto.randomUUID() })
    docSnapshot.data.returns(asset)
    return docSnapshot
  })
  Object.defineProperty(querySnapshot, 'docs', { value: docSnapshots })
  query.get.resolves(querySnapshot)
  collectionRef.where.withArgs('asset', '==', assetName).returns(query)
  firestore.collection.withArgs(`users/${userId}/assets`).returns(collectionRef)
  return { firestore, collectionRef, query, querySnapshot, docSnapshots }
}

In the documentation for Google Functions there is a number of great examples of using sinon to stub Firebase Realtime Database and Firestore. Its the ones listed as offline examples.

I believe they should be able to work in any Node.js context. Also express.

You can find them here: https://firebase.google.com/docs/functions/unit-testing

You wouldn't use sinon to mock this, you would make use of the @firebase/rules-unit-testing package to force interaction with a Firestore/RTDB emulator.

Related