A clear and concise introduction to testing Koa with Jest and Supertest

Photo Credit: Wikimedia Commons

Mocha and Chai are the way to go when it comes to testing a Node API but I couldn’t resist to give Jest a try.

As a web developer it’s crucial to be happy about your own code. But you should also prepare yourself to question your own approaches.

This is even more true in the Javascript’s land: the scene changes so fast! Sometimes that fancy new tool it’s worth a look, sometimes not. And Jest falls into the first category.

Requirements

To follow along you should have a basic understanding of Javascript and ES6.

Also, make sure to have one of the latest versions of Node. In the following post I’ll make use of the async/await pattern, introduced into Node 7.6.0.

Setting up the project

To start the project, create an empty directory named jest-koa2:

mkdir jest-koa2

then move inside it:

cd jest-koa2

and initialize package.json by running:

npm init -y

The -y flag configures package.json with the default values rather than asking us any questions.

We also need to create some directory structure. In your terminal type:

mkdir {test,server}

and hit enter.

Setting up Jest and Supertest

Jest markets itself as “delightful”. It must be true because many people are happy after transitioning from Mocha.

The Facebook’s testing framework was born for testing React. It supports Node as well, hence it’s suitable for testing both the frontend and the backend of a web application.

Install the package with:

npm i jest --save-dev

then update the script section inside package.json. The test command should point to the Jest executable and the testing environment should point to node:

"scripts": {
  "test": "jest"
},
"jest": {
  "testEnvironment": "node"
}

Jest is a testing framework, it does provide a platform for automated testing along with a basic assertion library (Expect). For APIs testing you must still rely on some external dependency: I chose supertest mainly because it supports promises, plus it’s lightweight. (On a side note, Expect was donated recently to the Jest team).

Install supertest with:

npm i supertest --save-dev

and you’re ready to go.

Setting up your first test

Create a new file named index.routes.test.js within the test directory:

// require the Koa server
const server = require("../server/index");
// require supertest
const request = require("supertest");

// close the server after each test
afterEach(() => {
  server.close();
});

describe("routes: index", () => {
  test("should respond as expected", async () => {
    const response = await request(server).get("/");
    expect(response.status).toEqual(200);
    expect(response.type).toEqual("application/json");
    expect(response.body.data).toEqual("Sending some JSON");
  });
});

In the above code I’m testing a basic API endpoint as it could be the root route /.

I’m assuming that:

  • the server replies with the expected status code
// ...
expect(response.status).toEqual(200);
//...
  • the server replies with valid JSON
// ...
expect(response.type).toEqual("application/json");
//...
  • the server returns the data as expected
// ...
expect(response.body.data).toEqual("Sending some JSON");
//...

More important, I’m able to use async/await inside the test block:

// ...
describe("routes: index", () => {
  test("should respond as expected", async () => {
    const response = await request(server).get("/");
    //
  });
});
//...

Note that async/await it’s not unique to Jest. If you run at least Node 7.6.0 the same pattern could work also with Mocha and Chai.

NOTE: you could also use the it() block instead of test()

With the first test in place run:

npm run test

to make it fail:

 FAIL  test/index.routes.test.js
  ● Test suite failed to run

    Cannot find module '../server/index' from 'index.routes.test.js'

      at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:192:17)
      at Object.<anonymous> (test/index.routes.test.js:1:105)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        0.612s
Ran all test suites.

Now all we have to do is create a basic Koa server to make the test pass.

Jest API testing fail
TDD’s first rule: write a test and make it fail

In comparison, here’s how the same test looks like in Mocha/Chai/Chai-Http from my previous article. (I’ve omitted the err check inside the Jest version):

const chai = require("chai");
const should = chai.should();
const chaiHttp = require("chai-http");
chai.use(chaiHttp);

  describe("routes: index", () => {
    it("should respond as expected", done => {
      chai
        .request(server)
        .get('/')
        .end((err, res) => {
          should.not.exist(err);
          res.status.should.eql(200);
          res.type.should.eql("application/json");
          res.body.data.eql("Sending some JSON")
          done();
        });
    });
  });

It’s slightly more verbose and does not use async/await.

Back to the server now!

Setting up Koa

Koa is a web framework for Node.js made by the same developers behind Express.

Unlike Express, Koa was born with minimalism in mind: it covers only the least possible. It does not even include a router! Some other great features are:

  • the ability to use async/await out of the box
  • the context, an object containing both the Node.js request and response objects. It is specific to Koa and provides a lot of convenient methods

Let’s move on by installing Koa along with the router:

npm i koa koa-router

Then create a new file named  index.js  inside the server/ directory:

const Koa = require("koa");
const Router = require("koa-router");
const router = new Router();

const app = new Koa();
const PORT = process.env.PORT || 8081;

router.get("/", async ctx => {
  ctx.body = {
    data: "Sending some JSON"
  };
});

app.use(router.routes());

const server = app.listen(PORT).on("error", err => {
  console.error(err);
});

module.exports = server;

This simple Koa server will respond on the / (root) route. It has one job: sending a JSON response to the user. In our case the “user” will be Jest. That’s a very contrived example but it’s enough to make the test pass.

NOTE: For a more complex case check out faq-app-api or koa-tdd-api

Run the test suite again:

npm test

and watch the test pass:

 PASS  test/index.routes.test.js
  routes: index
    ✓ should respond as expected (19ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.471s, estimated 1s
Ran all test suites.
Jest API testing pass
TDD’s second rule: write enough code to make the test pass

Testing an object with toHaveProperty

Let’s extend our test a little. What if we want to check whether an object contains some properties or not? toHaveProperty is exactly what we’re looking for!

The original test could be rewritten as follow:

//
describe("routes: index", () => {
  test("should respond as expected", async () => {
    const response = await request(server).get("/");
    expect(response.status).toEqual(200);
    expect(response.type).toEqual("application/json");
    expect(response.body.data).toEqual("Sending some JSON");
    expect(response.body.person).toHaveProperty("name");
    expect(response.body.person).toHaveProperty("lastname");
    expect(response.body.person).toHaveProperty("role");
    expect(response.body.person).toHaveProperty("age");
  });
});

but the above approach has a problem: the more properties you have to test, the more cluttered the test becomes. Object.keys could make it more readable:

//
describe("routes: index", () => {
  test("should respond as expected", async () => {
    const response = await request(server).get("/");
    expect(response.status).toEqual(200);
    expect(response.type).toEqual("application/json");
    expect(response.body.data).toEqual("Sending some JSON");
    expect(Object.keys(response.body.person)).toEqual(
      expect.arrayContaining(["name", "lastname", "role", "age"])
    );
  });
});

Run the test suite again:

npm test

and watch it fail:

 FAIL  test/index.routes.test.js
  ● routes: index › should respond as expected

    expect(object)[.not].toHaveProperty(path)

    Expected object to be an object. Received:
      undefined: undefined

      at Object.test (test/index.routes.test.js:14:35)
          at <anonymous>
      at process._tickCallback (internal/process/next_tick.js:188:7)

  routes: index
    ✕ should respond as expected (20ms)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.433s, estimated 1s
Ran all test suites.

To make the test pass we have to append a person object to ctx.body within the Koa route.

Open up server/index.js and apply the fix:

//
router.get("/", async ctx => {
  ctx.body = {
    data: "Sending some JSON",
    person: {
      name: "Valentino",
      lastname: "Gagliardi",
      role: "Web Developer",
      age: 32
    }
  };
});
//

Save and close the file then run the test again:

npm test

and watch it pass!

 PASS  test/index.routes.test.js
  routes: index
    ✓ should respond as expected (19ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.43s, estimated 1s
Ran all test suites.

Wondering what’s the next step? Dig deeper into the Jest documentation and start writing your own tests!

Where to go from here

Have you spotted any error in my code? Any suggestion? I would love to ear your input in the comments below!

For a more exhaustive example of testing with Jest check out this file.

Starting from the original project I replaced Mocha, Chai, and Chai-Http with Jest and Supertest. Here’s the commit

There are some caveats but the code looks more cleaner thanks to async/await. Plus there is no need to import any extra assertion library: Jest uses Expect.

I suggest the following readings to learn more about the transitioning from Mocha to Jest: Unlocking Test Performance — Migrating from Mocha to Jest and Why we’re migrating unit tests to Jest (and why you should too)

The code

The code for jest-koa2 is available on Github: jest-koa2

That’s all! Thanks for reading, see you next time!

Valentino Gagliardi

Valentino Gagliardi

Consultant, Developer Coach. Are you stuck on a project? Let's talk!
Valentino Gagliardi

5 Replies to “A clear and concise introduction to testing Koa with Jest and Supertest”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.