Blog logotrial and stderr

Testing Firebase Functions with Firestore

 • 

Figuring out how to set up testing for Firebase functions that use Firestore has been a nightmare. The Firebase docs have been out of date for almost a year. Here's how I got it working. My versions:

"firebase-admin": "^12.1.0",
"firebase-functions": "^6.0.1",
"firebase-functions-test": "^3.3.0",

and firebase (the cli) version 8.10.0. Hopefully this helps until the next major bump.

I'm trying to test a function

import { FirestoreEvent, onDocumentCreated } from "firebase-functions/v2/firestore";
import * as logger from "firebase-functions/logger";
import {initializeApp} from "firebase-admin/app";
import { getFirestore, QueryDocumentSnapshot } from "firebase-admin/firestore";

export const app = initializeApp({ projectId: 'my-app' });

type Event = FirestoreEvent<QueryDocumentSnapshot | undefined>;

export const myFunction = onDocumentCreated(
  "/products/{documentId}",
  async (event: Event) => {
    if (event.data) {
      const statusDoc = getFirestore().doc(`/${event.document}/statuses/current`);
      const status = await statusDoc.get();
      logger.log(event.data.data());
      logger.log(status.data());
    }
  }
);

Using the npm script

"test": "firebase emulators:exec --only firestore jest"

I run the following test

import * as admin from 'firebase-admin';
import * as logger from "firebase-functions/logger";
import * as firebaseFunctionsTest from 'firebase-functions-test';

const test = firebaseFunctionsTest({ projectId: 'my-app' });

// I read somewhere that it's important to do the require after
// initializing `firebase-functions-test`, so that it will pick
// up some configuration overrides or something. It doesn't seem
// to matter in my setup, but maybe it will for yours.
const { myFunction } = require('./index');

const db = admin.firestore();

describe('myFunction', () => {
  let loggerSpy: jest.SpyInstance;

  beforeAll(() => {
    if (!admin.apps.length) {
      admin.initializeApp();
    }
  });

  beforeEach(() => {
    loggerSpy = jest.spyOn(logger, 'log');
  });

  afterAll(async () => {
    await Promise.all(admin.apps.map(app => app!.delete()));
    await deleteCollection('products');
    await deleteCollection('statuses');
  });

  it('runs my function and gets both event and status data', async () => {
    wrapped = test.wrap(myFunction);
    const mockProduct: any = {
      name: 'Widget 2000'
    };
    const { productSnapshot } = await createProduct(mockProduct);
   
    // This event just gets passed through to the function
    const event = { data: productSnapshot, document: 'products/product1' }
    
    await wrapped(event);

    expect(loggerSpy).toHaveBeenCalledWith(expect.any(Object));
  });
});


/** This function creates the event and status documents
 *  in the emulated firestore */
export async function createProduct(product: object) {
  const status = { status: "pending" };
  const productRef = db.collection('products').doc('product1')
  await productRef.set(product);
  const statusRef = productRef.collection('statuses').doc('current');
  await statusRef.set(status);
  const productSnapshot = await productRef.get();
  const statusSnapshot = await statusRef.get();
  return { productSnapshot, statusSnapshot };
}

/** Clean up! */
export async function deleteCollection(collection: string) {
  const snapshot = await db.collection(collection).get()
  for (const doc of snapshot.docs) {
    await doc.ref.delete();
  }
}

Since we are using the firestore emulator, we can be confident that the function behavior will be pretty realistic.

Figuring out what went wrong

 • 

I was working on a large refactor with hundreds of changed files, with six commits on the branch past main. But somehow something had broken that seemed totally unrelated to my work! When I would git checkout main everything was fine, and when I would git checkout mybranch it had broken. Specifically, some Typescript types for a third-party library were no longer resolving—and my changes involved neither the third party library nor the Typescript setup.

First thing I did was see if I had changed any of the build config files, anything at the top level that could make strange things happen. git checkout main yarn.lock and things like that. No dice.

How I ended up resolving the issue was the following:

  1. Create a new branch, git checkout -b mybranch-debug
  2. Do an interactive rebase with git rebase -i and fixup everything into a single commit
  3. Split that commit into one commit per file using this script, resulting in hundreds of commits.
  4. git bisect to find the guilty file
  5. Delete stuff from that file until it's evident what caused the problem

Hope that helps someone else figure their way out of a baffling problem!

React Hook debugging hack

 •  Filed under react

Want a quick, no-lib hack to make it easier to track which instances of a hook are doing what? Use useRef.

function useMyHook() {
  const id = useRef(Math.floor(Math.random() * 1000));
  console.group(`useMyHook ${id.current}`);
  ...
  console.groupEnd();
}

Linting uninitialized field dereference in Java

 •  Filed under java

Am I out of my mind for thinking that some linter or other should be able to recognize the following code as buggy?

package com.company;

public class Main {

    public String greeting = "hey";

    private Main anotherMain;

    public Main getAnotherMain() {
        return anotherMain;
    }

    public void setAnotherMain(Main main) {
        this.anotherMain = main;
    }

    public static void main(String[] args) {
        Main main = new Main();
        System.out.println(main.getAnotherMain().greeting);
    }
}

I expect a linter to tell me that the unchecked dereference main.getAnotherMain().greeting could produce an NPE because anotherMain defaults to null.

I tried SpotBugs, NullAway, and the IntelliJ static checker, and none of them can catch this. Bug filed against SpotBugs. We'll see how it goes.

Choose unique names for test fixtures

 •  Filed under testing, art

In a library I maintain, I wrote all the tests using names from the foo-bar-baz family in fixtures.

I realized that having 50 tests where all the test data is foo-bar-baz makes it harder to debug. Adding a console.log to a line that gets hit 50 times might yield variations on { foo: "bar" } for all 50 tests, but only one corresponds to the error I'm interested.

That's why I've switched to pulling random names from a less limited domain. For this library, I'm using Japanese food names. It doesn't matter what they are or what they mean. What matters is that I can search my test logs for tamago and, if I've only used tamago in one test, immediately find the data corresonding to the test I'm interested in, { tamago: { kake: "gohan" } }. Delicious.

NPE

 •  Filed under java

Type safety without null safety is like a biohazard suit with an open face.

This is very typical Java code. It exhibits obnoxiously verbose typing, and yet can crash your application with NullPointerExceptions at runtime.

SuperDatabaseConnector superDatabaseConnector = SuperDatabaseConnector.getInstance();

getResultsFrom(superDatabaseConnector);

public static SomethingElseButMaybeNull getResultsFrom(SuperDatabaseConnector superDatabaseConnector) {
  return superDatabaseConnector.getResults();
}

Java is a bad language and you shouldn't use it.

Merging packages and their histories into a Lerna monorepo

 •  Filed under git

Update: You probably just want to use lerna import. But if you'd like a manual technique, or aren't actually using Lerna, read on. Let's say you have a Lerna monorepo called acme. You have a package called app, presently in its own repository, that you want to move into repo. read on...