DEV Community

w
w

Posted on • Originally published at jaywolfe.dev on

Setup A Fastify App with Jest Tests the Right Way ⚡️

Setup A Fastify App with Jest Tests the Right Way ⚡️

If you've ever used the fastify-cli to generate a new fastify app only to find out it uses a package called "tap" to write your tests, you might have been dissappoined. I also shared this dissappointment. While there are many good reasons to use tap instead of jest for your tests in your fastify app, it may be more than you're trying to "bite off" when first learning the fastify ecosystem. If this sounds similar to your experience, then look no further - I have you covered. Setup Jest tests in a brand new Fastify app the right way with this short guide.

By the way, if you prefer video tutorials or jumping straight into the code examples yourself, check them out here:

Video Tutorial of this Article

GitHub Code

Get Going With A Fastify App

A quick run of the following will get you running with a new fastify app:

npm i -g fastify-cli;
fastify generate fastify-setup-with-jest --lang=ts;
cd fastify-setup-with-jest && npm i;

Enter fullscreen mode Exit fullscreen mode

That should get you a standard fastify typescript boilerplate app. We'll take on the next steps a couple at a time.

Getting Jest Dependencies

Even though right now we have a starter app with tap tests, we can pretty quickly swap tap out for jest. First lets get those jest dependencies installed:

npm i jest ts-jest @types/jest;
npx ts-jest config:init;

Enter fullscreen mode Exit fullscreen mode

This should get you setup with required jest dependencies and a simple jest config file to allow running your tests with ts-jest and typescript. Now all we need to do is complete a slight refactor of our tap formatted tests and we're done!

Swapping Tap for Jest - Test Setup

You'll notice we have integration tests - tests that actually "call" our endpoints within our fastify app via fastify.inject. These can be accommodated via a simple refactor to the helper.ts file. We just need to use jest lifecycle methods instead.

// helper.ts

import Fastify from "fastify";
import fp from "fastify-plugin";
import App from "../src/app";

export function build() {
  const app = Fastify();

  beforeAll(async () => {
    void app.register(fp(App));
    await app.ready();
  });

  afterAll(() => app.close());

  return app;
}

Enter fullscreen mode Exit fullscreen mode

Notice how we have a synchronous function that returns a fastify app instance while we can use jest's asynchronous lifecycle functions beforeAll and afterAll to register our app and it's plugin dependencies and wait for everything to be ready. This way we can get the fastify app instance within our test file describe blocks without having to actually do any asynchronous work, or messy let declarations with re-assignments. It's simply much cleaner this way.

We can also use the afterAll hook to correctly shutdown the fastify instance. One last point of note - we don't actually need to call .listen() on the app instance thanks to the excellent API provided by fastify's core method fastify.inject which we'll see in our test files.

Now that we have the main way to instantiate our fastify server for integration tests the "jest" way, we can refactor our actual tests!

Refactoring the Tests

When refactoring all of the existing tests, the changes we have to make fall into one of three categories.

Removing import references to tap like the following:

import { test } from "tap";

Enter fullscreen mode Exit fullscreen mode

Removing the t parameter from the test function callbacks:

test("default root route", async (t) => {

Enter fullscreen mode Exit fullscreen mode

This is actually paramount - if you don't do this then jest will think you've used the done callback style of test and your tests will timeout. If you accidentally do this without realizing it will be hard to debug. Ask me how I know 😉.

Updating the t based assertions to normal jest assertions:

From:

t.same(JSON.parse(res.payload), { root: true });

Enter fullscreen mode Exit fullscreen mode

To:

expect(res.json()).toEqual({ root: true });

Enter fullscreen mode Exit fullscreen mode

Lastly, since most of your test files only have one test, you'll see that the "testing" version of the fastify app (think back to our build() function from our helpers.ts file) gets instantiated within the test function blocks directly like so:

import { build } from "../helper";

test("default root route", async () => {
  const app = build();
  const res = await app.inject({
    url: "/",
  });
  expect(res.json()).toEqual({ root: true });
});

Enter fullscreen mode Exit fullscreen mode

This will work fine when there's only one test, however if you have multiple tests per file, re-instantiating the "testing" version of the fastify server is an expensive operation. You can re-use the existing test app instance by doing the following:

import { build } from "../helper";

const app = build();

test("default root route", async () => {
  const res = await app.inject({
    url: "/",
  });
  expect(res.json()).toEqual({ root: true });
});

test("some other root test", async () => {
  const res = await app.inject({ url: "/root2" });

  expect(res.json()).toEqual({ root2: true });
});

Enter fullscreen mode Exit fullscreen mode

Simple as that! Now you can write multiple tests per file against your "test bed" version of your fastify app with really clean and simple syntax.

Conclusion

Thanks for tuning in, I hope you found this technique as helpful as I did. If you're interested in a video tutorial version of this article, check out my video walkthrough of this technique here:

Video Tutorial of this Article

GitHub Code

Top comments (1)

Collapse
 
aroramayank2002 profile image
Mayank Arora

After making the changes I get:
Error: Hooks cannot be defined inside tests. Hook of type "beforeAll" is nested within "default root route".