Skip to content Skip to sidebar Skip to footer

What's The Best Way To Test Express.js Api

I'm new in API testing with JavaScript. I've found many solution for testing REST APIs, but not sure what's the best. Im using express.js in the backend and for the tests jest. I'v

Solution 1:

It's impossible to mock getData as myapp.getData. module.exports.getData = getData doesn't serve a good purpose for testing because it allows to mock getData only if it's used everywhere as exports.getData() instead of getData(), which is impractical.

data needs to be mocked in a module it's defined. Then all modules that depend on it should be re-imported per test in order to be affected by a mock. jest.isolateModules or jest.resetModules can be used in conjunction with require to provide test-specific module mocks.

beforeEach(() => {
  jest.resetModules();
});

it('should return 2 lists', async () => {
  jest.mock('.../data', () => [...]);
  const myapp = require('.../app');

  const res = awaitrequest(myapp)
  ...
});

Solution 2:

I think I've reached the setup I wanted:

  • await everywhere, no infinitely deeply nested callbacks
  • no external libraries besides mocha and express, an analogous setup would work for other systems
  • no mocking. No mocking extra effort. It just starts a clean server on a random port for each test, and closes the server at the end of the test, exactly like the real thing

Not shown in this example, you would want to finish things of by running the app with a temporary in-memory SQLite database unique to every test when NODE_ENV=test is used. The real production server would run on something like PostgreSQL, and an ORM like sequelize would be used so that the same code works on both. Or you could have a setup that creates the DB once and truncates all tables before each test.

app.js

#!/usr/bin/env nodeconst express = require('express')

asyncfunctionstart(port, cb) {
  const app = express()
  app.get('/', (req, res) => {
    res.send(`asdf`)
  })
  app.get('/qwer', (req, res) => {
    res.send(`zxcv`)
  })
  returnnewPromise((resolve, reject) => {
    const server = app.listen(port, asyncfunction() {
      try {
        cb && awaitcb(server)
      } catch (e) {
        reject(e)
        this.close()
        throw e
      }
    })
    server.on('close', resolve)
  })
}

if (require.main === module) {
  start(3000, server => {
    console.log('Listening on: http://localhost:' + server.address().port)
  })
}

module.exports = { start }

test.js

const assert = require('assert');
const http = require('http')

const app = require('./app')

functiontestApp(cb) {
  return app.start(0, async (server) => {
    awaitcb(server)
    server.close()
  })
}

// https://stackoverflow.com/questions/6048504/synchronous-request-in-node-js/53338670#53338670functionsendJsonHttp(opts) {
  returnnewPromise((resolve, reject) => {
    try {
      let body
      if (opts.body) {
        body = JSON.stringify(opts.body)
      } else {
        body = ''
      }
      const headers = {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body),
        'Accept': 'application/json',
      }
      if (opts.token) {
        headers['Authorization'] = `Token ${opts.token}`
      }
      const options = {
        hostname: 'localhost',
        port: opts.server.address().port,
        path: opts.path,
        method: opts.method,
        headers,
      }
      const req = http.request(options, res => {
        res.on('data', data => {
          let dataString
          let ret
          try {
            dataString = data.toString()
            if (res.headers['content-type'].startsWith('application/json;')) {
              ret = JSON.parse(dataString)
            } else {
              ret = dataString
            }
            resolve([res, ret])
          } catch (e) {
            console.error({ dataString });
            reject(e)
          }
        })
        // We need this as there is no 'data' event empty reply, e.g. a DELETE 204.
        res.on('end', () =>resolve([ res, undefined ]))
      })
      req.write(body)
      req.end()
    } catch (e) {
      reject(e)
    }
  })
}

it('test root', () => {
  // When an async function is used, Mocha waits for the promise to resolve// before deciding pass/fail.returntestApp(async (server) => {
    let res, data

    // First request, normally a POST that changes state.
    ;[res, data] = awaitsendJsonHttp({
      server,
      method: 'GET',
      path: '/',
      body: {},
    })
    assert.strictEqual(res.statusCode, 200)
    assert.strictEqual(data, 'asdf')

    // Second request, normally a GET to check that POST.
    ;[res, data] = awaitsendJsonHttp({
      server,
      method: 'GET',
      path: '/',
      body: {},
    })
    assert.strictEqual(res.statusCode, 200)
    assert.strictEqual(data, 'asdf')
  })
})

it('test /qwer', () => {
  returntestApp(async (server) => {
    let res, data
    ;[res, data] = awaitsendJsonHttp({
      server,
      method: 'GET',
      path: '/qwer',
      body: {},
    })
    assert.strictEqual(res.statusCode, 200)
    assert.strictEqual(data, 'zxcv')
  })
})

package.json

{"name":"tmp","version":"1.0.0","dependencies":{"express":"4.17.1"},"devDependencies":{"mocha":"6.2.2"},"scripts":{"test":"mocha test test.js"}}

With this, running:

npm install
./app

runs the server normally as desired. And running:

npm test

makes the tests work as desired. Notably, if you hack any of the asserts to wrong values, they will throw, the server closes without hanging, and at the end failing tests show as failing.

The async http requests are also mentioned at: Synchronous request in Node.js

Tested on Node.js 14.17.0, Ubuntu 21.10.

Post a Comment for "What's The Best Way To Test Express.js Api"