Unit test your DB with GraphQL and Jest

Matt Krick
Parabol Focus
Published in
7 min readMar 8, 2017

--

Recently, I built out billing and invoicing functionality for our small, open-source SaaS app (look out world, we got ourselves a business model!). The added functionality expanded the codebase by something like 45% and errors crept in like gophers to a carrot patch. Now, we’re already pretty good about reactively handling errors. We use redux-middleware to send the error & redux state to Sentry, which grabs our sourcemaps & pings us on our Slack channel. But with billing, I want to catch the error before it happens… because refunds are embarrassing. Unfortunately, unit testing a database has always been a pain. After all, a unit test is meant for a small, stateless function, and there’s nothing stateless about a database.

Why unit testing a DB sucks

Let’s name all the ways that unit testing can be a real pain in the DB:

  • All your tests share the same database (goodbye concurrent testing!)
  • Compared to in-memory arrays, it’s dead slow
  • 1 mutation can change many fields on many tables, meaning 50+ individual assertions is not uncommon
  • Asserting UserX is on TeamY is hard when both have non-deterministic unique IDs
  • Achieving the right pre-test state of the DB is a lengthy, painful, error-prone process that often takes longer than writing the business logic itself

Seems like quite a mountain to climb! Let’s dig in.

The Premise

There are enough 101’s in the world, let’s do something that actually occurs in the wild. Let’s say we’ve got a mutation that updates a customer’s credit card number. We receive a card token (because there’s no way I want the added burden of safely handling your actual credit card), send it to Stripe, get back the last 4 digits, and update it in our DB. Seems simple enough! Unfortunately, getting the database (and Stripe) to a state that can handle this scenario is painful. We have to create a User. We need that user to have an Organization with a valid credit card. That Organization needs to be a valid Stripe Customer and have a valid Stripe Subscription. Oh yeah, and we need this test to be lighting fast, because we’ve got a lot more.

Setting up the database

First, we’ll need a database just for testing. If you’re like me, you already have a series of database migrations set up so you can safely update and rollback your DB in production. If not, you probably have some kind of function that you can call to create a bunch of tables and indexes. Simply make sure the database used is a function of an environment variable (eg NODE_ENV="test") and run it.

Second, we’ll want to speed up the database by softening the write durability. While having a hard durability (making sure the doc was written to disk) is ideal for production, a soft durability (assuming the doc got written to disk) is much faster. Since this is only needed when you initially set up your test database, I run it directly after the migration. Combined, the migration and softening is like a webpack DLL: you only need to run it once when you install the repo or make a major change.

Finally, we’ll need to teardown the database and remove the records. Using an afterAll function in Jest won’t work because it’s run after all the tests in a file complete, so you might be wiping out rows of data that another test still needs. Instead, we want to trigger something after all the tests complete. It’s a little unintuitive, but you’ll need to write a script and reference it in your Jest config’s testResultsProcessor. In that teardown, I like to drain the DB connection pool so the process exits clean and every so often I empty out all the records — do this too much and it becomes added overhead, never do it and you’ve got a huge DB on your test machine, so I settle on about once every 100 runs.

Designing the test

A GraphQL mutation is the perfectly sized chunk for unit tests. Sure, you’ll still want to mock the occasional function in your mutations, but starting with the mutation as a whole is a great start. If you notice the test suite is getting too unruly, it’s a good cue to ask yourself if your mutation itself is too big.

If you’ve read my tips on using GraphQL in production, then you know about the Auth, Validation, Resolution pattern for GraphQL mutations. Similarly, I break each unit test into Setup, Test, and Verify stages (props to Jordan Husney for the idea).

The Setup stage

Setup is used to get the database to a useful pre-test state. In this example, it means creating a user and organization. Now, there are 2 ways for us to create a user and organization: we could use our pre-existing business logic (presumably createUser and createOrg mutations) or we could directly insert some rows into the database. The former is DRY, but since it depends on other mutations, it turns into a slow integration test rather than a fast unit test. So, I opt for the latter. To DRY it up, I use an app-specific MockDB I built which gives me a nice clean API:

const mockDB = new MockDB(); 
const {user, organization} = await mockDB
.newUser()
.newOrganization({creditCard})

The MockDB is nothing fancy: each method call pushes a new document with default values into an in-memory array. If I need to add more fields or override the default values (like I do with creditCard above) I just pass them in. Then, when you stick await in front of it, it magically batch-inserts the in-memory arrays to my database all at once, making for the quickest setup imaginable.

The Test stage

The Test stage is simply preparing the variables from the Setup stage, calling the GraphQL mutation, and awaiting the result to mutate your DB. In this case, await addBilling.resolve(...). It’s rare if this is more than couple lines.

The verify stage

This is where we rip the data from the database and run our assertions on it. That means we need to grab the Organization and make sure it looks exactly right. We could do this by cherry picking a couple fields like creditCard and updatedAt, but in reality we’re gonna update about 20 fields across 3 tables, which is enough cherry picking to make a pie.

Look at all those ooey gooey assertions just oozing out the sides of our unit test.

Instead, let’s use Jest’s snapshot feature. It’s like a JSON deep equals, but it generates and maintains the expected value for you (why folks never explain it like this & opt for a cute polaroid analogy is beyond me).

So what do we want to snapshot? The Organization doc, any User in that organization, and eventually our Stripe account (yeah, I made a manual mock of Stripe, but let’s save that for another time):

const queriesToSnapshot = {
organization: db.table('Organization').get(orgId),
user: db.table('User').getAll(orgId, {index: 'orgId'})
}
const keys = Object.keys(queriesToSnapshot);
const values = Object.values(queriesToSnapshot);
const docs = await Promise.all(values);
const snapshot = docs.map((doc, idx) => {
return dynamicSerializer.toStatic(doc, fieldsToSerialize[idx])
})

By creating an object full of promises like queriesToSnapshot, we can fetch all the documents concurrently. That means Setup, Test, and Verify each only call await once, keeping our tests screaming fast. Then, we string together all the results into one big JSON so we only run 1 snapshot per DB. But what exactly is that dynamicSerializer?

The Dynamic Serializer

Every time this test is run, it will generate new unique values for things like userId. This is necessary because our tests are going to run in parallel and we don’t want 2 primary keys to collide when the documents are inserted. However, random keys make snapshot testing impossible. Furthermore, it is not enough to just remove those fields. For example, my user document has an orgId foreign key that I want to match to its primary key on Organization.

We could solve this by mocking our random key generator in each test and very carefully stringing together a chain of mockReturnValueOnce, but then our tests get bloaty and brittle. Instead, I solved this the same way webpack did with numbered modules: create a cache of all unique keys, and then replace them with an incrementing integer. But there’s another constraint — what if I have more than 1 database in the same test? For example, if my stripeCustomerId serializes to 5 in my DB, I want it to serialize to 5 in my mock of Stripe. Thankfully, that’s all handled for me in a package called dynamic-serializer, which manages a dynamic value dictionary for the entire test. And just like that, I have deterministic unique IDs!

Conclusion

On my vintage 2013 Macbook Air, a single unit test involving 3 tables takes about 30ms — pretty darn reasonable. Each test is also about 20 LOCs, which makes it easy to grok when I inevitably break my tests and have to revisit my tests in the coming month. More importantly, by testing my actual queries against a real database output, I uncovered a handful of bugs and edge cases that would have otherwise gone unnoticed. If you want to see more, feel free to dig into the code. If you hate what you see, drop me a line in the comments below.

--

--