Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit testing Cloud Functions for Firebase: what's the "right way" to test/mock `transaction`s with sinon.js

Man, this firebase unit testing is really kicking my butt.

I've gone through the documentation and read through the examples that they provide, and have gotten some of my more basic Firebase functions unit tested, but I keep running into problems where I'm not sure how to verify that the transactionUpdated function passed along to the refs .transaction is correctly updating the current object.

My struggle is probably best illustrated with their child-count sample code and a poor attempt I made at writing a unit test for it.

Let's say my function that I want to unit test does the following (taken straight from that above link):

// count.js
exports.countlikechange = functions.database.ref('/posts/{postid}/likes/{likeid}').onWrite(event => {
      const collectionRef = event.data.ref.parent;
      const countRef = collectionRef.parent.child('likes_count');

      // ANNOTATION: I want to verify the `current` value is incremented
      return countRef.transaction(current => {
        if (event.data.exists() && !event.data.previous.exists()) {
          return (current || 0) + 1;
        }
        else if (!event.data.exists() && event.data.previous.exists()) {
          return (current || 0) - 1;
        }
      }).then(() => {
        console.log('Counter updated.');
      });
    });

Unit Test Code:

const chai = require('chai');
const chaiAsPromised = require("chai-as-promised");
chai.use(chaiAsPromised);
const assert = chai.assert;
const sinon = require('sinon');

describe('Cloud Functions', () => {
  let myFunctions, functions;

  before(() => {
    functions = require('firebase-functions');
    myFunctions = require('../count.js');
  });

  describe('countlikechange', () => {
    it('should increase /posts/{postid}/likes/likes_count', () => {
      const event = {
        // DeltaSnapshot(app: firebase.app.App, adminApp: firebase.app.App, data: any, delta: any, path?: string);
        data: new functions.database.DeltaSnapshot(null, null, null, true)
      }

      const startingValue = 11
      const expectedValue = 12

      // Below code is misunderstood piece.  How do I pass along `startingValue` to the callback param of transaction
      // in the `countlikechange` function, and spy on the return value to assert that it is equal to `expectedValue`?
      // `yield` is almost definitely not the right thing to do, but I'm not quite sure where to go.
      // How can I go about "spying" on the result of a stub,
      // since the stub replaces the original function?
      // I suspect that `sinon.spy()` has something to do with the answer, but when I try to pass along `sinon.spy()` as the yields arg, i get errors and the `spy.firstCall` is always null. 
      const transactionStub = sinon.stub().yields(startingValue).returns(Promise.resolve(true))

      const childStub = sinon.stub().withArgs('likes_count').returns({
        transaction: transactionStub
      })
      const refStub = sinon.stub().returns({ parent: { child: childStub }})

      Object.defineProperty(event.data, 'ref', { get: refStub })

      assert.eventually.equals(myFunctions.countlikechange(event), true)
    })
  })
})

I annotated the source code above with my question, but I'll reiterate it here.

How can I verify that the transactionUpdate callback, passed to the transaction stub, will take my startingValue and mutate it to expectedValue and then allow me to observe that change and assert that it happened.

This is probably a very simple problem with an obvious solution, but I'm very new to testing JS code where everything has to be stubbed, so it's a bit of a learning curve... Any help is appreciated.

like image 206
tim Avatar asked Dec 23 '22 17:12

tim


1 Answers

I agree that unit testing in the Firebase ecosystem isn't as easy as we'd like it to be. The team is aware of it, and we're working to make things better! Fortunately, there are some good ways forward for you right now!

I suggest taking a look at this Cloud Functions demo that we've just published. In that example we use TypeScript, but this'll all work in JavaScript too.

In the src directory you'll notice we've split out the logic into three files: index.ts has the entry-logic, saythat.ts has our main business-logic, and db.ts is a thin abstraction layer around the Firebase Realtime Database. We unit-test only saythat.ts; we've intentionally kept index.ts and db.ts really simple.

In the spec directory we have the unit tests; take a look at index.spec.ts. The trick that you're looking for: we use mock-require to mock out the entire src/db.ts file and replace it with spec/fake-db.ts. Instead of writing to the real database, we now store our performed operations in-memory, where our unit test can check that they look correct. A concrete example is our score field, which is updated in a transaction. By mocking the database, our unit test to check that that's done correctly is a single line of code.

I hope that helps you do your testing!

like image 181
Robert-Jan Huijsman Avatar answered Dec 28 '22 11:12

Robert-Jan Huijsman