A recommended way to do functional testing with K.O architecture
Tags: express, productivity, testing, architecture
Published: 2019-11-08 03:17:46

In my last article, I introduced a new architecture pattern to improve the productivity of express.js API development. Functional testing with the new architecture requires another article to discuss, so here it is.

## How to read this article
It is recommended to read my previous article before continuing.
{% link dividedbynil/a-minimalist-architecture-pattern-for-expressjs-api-applications-nee %}

## Demo project
Let's reuse the last project that we created.
{% github dividedbynil/ko-architecture %}

## Prerequisite

### Import testing libraries
```js
npm i -D mocha chai
```
Here we choose [mocha](https://mochajs.org/) as the testing framework, [chai](https://www.chaijs.com/) as the assertion library.

Let's add a test script in `package.json`.
```
"scripts": {
  ...
  "test": "./node_modules/mocha/bin/mocha"
},
```

## Unit testing

### `./common/response.js`
```js
function errorRes (res, err, errMsg="failed operation", statusCode=500) {
  console.error("ERROR:", err)
  return res.status(statusCode).json({ success: false, error: errMsg })
}

function successRes (res, data, statusCode=200) {
  return res.status(statusCode).json({ success: true, data })
}

...
```
The `res` arguments in the functions above are slightly tricky for unit testing. However, it is not difficult to mock `res`.

For example (the code below is using jest testing framework):
```js
const { errData, errorRes, successRes } = require('./response')

// mocking res
const res = () => ({
  err: '',
  errMsg: '',
  statusCode: 0,
  result: {},
  status (code) {
    this.statusCode = code
    return this
  },
  json (obj) {
    this.result = obj
    return this
  }
})

describe('errorRes', () => {
  it('should return default error status code', () => {
    const obj = errorRes(res(), '')
    expect(obj.statusCode).toBe(500)
  })

  it('should return custom error status code', () => {
    const obj = errorRes(res(), '', '', 401)
    expect(obj.statusCode).toBe(401)
  })

  it('should return default errMsg', () => {
    const obj = errorRes(res(), '')
    expect(obj.result.error).toBe('failed operation')
  })

  it('should return custom errMsg', () => {
    const errMsg = 'invalid operation'
    const obj = errorRes(res(), '', errMsg)
    expect(obj.result.error).toBe(errMsg)
  })
})
...
```
### `common/crud.js`
```js
const { errData, errorRes, successRes } = require('../common/response')
const mongoose = require('mongoose')


function create (model, populate=[]) {
  return (req, res) => {
    const newData = new model({
      _id: new mongoose.Types.ObjectId(),
      ...req.body
    })
    return newData.save()
      .then(t => t.populate(...populate, errData(res)))
      .catch(err => errorRes(res, err))
  }
}

function read (model, populate=[]) {
  return (req, res) => (
    model.find(...req.body, errData(res)).populate(...populate)
  )
}
...
```
The `model` arguments in the functions above are not that simple to do mocking. Feel free to try it out and leave a code snippet in the comment if you made it.


## A recommended way

A recommended way to test K.O architecture is to use an HTTP assertion library to do bottom-up integration testing. By doing so we can avoid creating complex mock functions.

### Import http assertion library - [supertest](https://github.com/visionmedia/supertest)
```js
npm i -D supertest
```

Let's create a minimal express `app` to test the functions in `common/response.js`.

### test/response.test.js
```js
const { errData, errorRes, successRes } = require('../common/response')
const request = require('supertest');
const bodyParser = require('body-parser');
const express = require('express');
const app = express();

app
.use(bodyParser.json())
.post('/errorRes', (req, res) => errorRes(res, ...req.body))
.post('/successRes', (req, res) => successRes(res, ...req.body))
.post('/errData', (req, res) => {
  const { errMsg, err } = req.body
  errData(res, errMsg) (err, { custom: true })
})

```
Using the `post` HTTP method is more convenient as we utilize `req.body` as the function argument carrier.

Let's test the express app.

### test/response.test.js
```js
function test (funcStr, args) {
  return request(app)
          .post('/'+funcStr)
          .send(args)
}

describe ('response', function () {
  describe('errorRes', function () {
    it('should return default status code & message', function (done) {
      test('errorRes', ['error'])
        .expect(500, {
          success: false,
          error: 'failed operation'
        }, done)
    })

    it('should return custom errMsg', function (done) {
      test('errorRes', ['error', 'test'])
        .expect(500, {
          success: false,
          error: 'test'
        }, done)
    })

    it('should return custom errMsg & status code', function (done) {
      test('errorRes', ['error', 'test', 401])
        .expect(401, {
          success: false,
          error: 'test'
        }, done)
    })
  })

  describe('successRes', function () {
    it('should return default status code & data', function (done) {
      test('successRes', [])
        .expect(200, {
          success: true,
          data: {}
        }, done)
    })

    it('should return custom data', function (done) {
      test('successRes', [{ custom: true }])
        .expect(200, {
          success: true,
          data: { custom: true }
        }, done)
    })

    it('should return custom data & status code', function (done) {
      test('successRes', [{ custom: true }, 201])
        .expect(201, {
          success: true,
          data: { custom: true }
        }, done)
    })
  })

  describe('errData', function () {
    it('should return default status code & error', function (done) {
      test('errData', { err: true })
        .expect(500, {
          success: false,
          error: 'failed operation'
        }, done)
    })

    it('should return default status code & data', function (done) {
      test('errData', { err: false })
        .expect(200, {
          success: true,
          data: { custom: true }
        }, done)
    })

    it('should return custom error message', function (done) {
      test('errData', { err: true, errMsg: 'custom' })
        .expect(500, {
          success: false,
          error: 'custom'
        }, done)
    })
  })
})
```

Let's create another minimal express `app` to test functions in `common/crud.js`.

### test/curd.test.js
```js
const { create, read, update, remove } = require('../common/crud')
const Restaurant = require('../models/Restaurant')
const User = require('../models/User')
const request = require('supertest')
const expect = require('chai').expect
const bodyParser = require('body-parser')
const express = require('express')
const mongoose = require('mongoose')
// check the updated github readme for example
const { testMongoUrl } = require('../config')
const app = express()

mongoose.connect(testMongoUrl, {
  useNewUrlParser: true,
  autoIndex: false,
  useFindAndModify: false,
  useUnifiedTopology: true,
})

app
.use(bodyParser.json())
.post('/create', create(Restaurant, ['owner']))
.post('/read', read(Restaurant, ['owner']))
.post('/update/:_id', update(Restaurant, ['owner']))
.post('/remove/:_id', remove(User))
```

Using a new mongo database (`testMongoUrl`) to do testing is more convenient to remove testing data after each test run and not interfere with existing data.

```js
function test (funcStr, args) {
  return request(app)
          .post('/'+funcStr)
          .send(args)
}

describe('crud', function () {
  const user = {
    "name": "Test",
    "email": "test@test.com",
    "type": "admin",
    "password": "12345"
  }

  const restaurant = {
    "name": "Brand New Restaurant",
    "location": {
      "type": "Point",
      "coordinates": [-73.9983, 40.715051]
    },
    "available": true
  }

  const testUser = new User({
    _id: new mongoose.Types.ObjectId(),
    ...user
  })

  before(function (done) {
    testUser.save(done)
  })

  after(function (done) {
    mongoose.connection.db.dropDatabase(function(){
      mongoose.connection.close(done)
    })
  })

  describe('create', function () {
    it('should not create new restaurant without owner', function (done) {
      const data = restaurant
      test('create', data)
        .expect(500, done)
    })
    it('should return new restaurant and populate', function (done) {
      const data = { owner: testUser._id, ...restaurant }
      test('create', data)
        .expect(200)
        .expect(function (res) {
          restaurant._id = res.body.data._id
          expect(res.body.data).to.be.a('object')
          expect(res.body.data).to.deep.include(restaurant)
          expect(res.body.data.owner.name).to.equal('Test')
          expect(res.body.data.owner.password).to.equal(undefined)
        })
        .end(done)
    })
  })

  describe('read', function () {
    it('should return restaurant and populate', function (done) {
      const data = [{ "available": true, _id: restaurant._id }]
      test('read', data)
        .expect(200)
        .expect(function (res) {
          expect(res.body.data).to.be.a('array')
          expect(res.body.data[0]).to.deep.include(restaurant)
          expect(res.body.data[0].owner.name).to.equal('Test')
          expect(res.body.data[0].owner.password).to.equal(undefined)
        })
        .end(done)
    })
    it('should return no restaurant', function (done) {
      const data = [{ "available": false }]
      test('read', data)
        .expect(200)
        .expect(function (res) {
          expect(res.body.data.length).to.equal(0)
        })
        .end(done)
    })
  })

  describe('update', function () {
    it('should return updated restaurant and populate', function (done) {
      const { _id } = restaurant
      const data = { name: 'New name' }
      test('update/'+_id, data)
        .expect(200)
        .expect(function (res) {
          expect(res.body.data).to.be.a('object')
          expect(res.body.data).to.deep.include({...restaurant, ...data})
          expect(res.body.data.name).to.equal('New name')
          expect(res.body.data.owner.name).to.equal('Test')
          expect(res.body.data.owner.password).to.equal(undefined)
        })
        .end(done)
    })
  })

  describe('remove', function () {
    it('should remove user', function (done) {
      const { _id } = testUser
      test('remove/'+_id)
        .expect(200)
        .expect(function (res) {
          expect(res.body.data).to.be.a('object')
          expect(res.body.data).to.have.property('ok')
          expect(res.body.data.ok).to.equal(1)
        })
        .end(done)
    })
  })

})
```

Let's create another minimal express `app` to test functions in `common/middleware.js`.

### test/middleware.test.js
```js
const { notFound, onlyAdmin, notOnlyMember } = require('../common/middleware')
const request = require('supertest');
const bodyParser = require('body-parser');
const express = require('express');
const app = express();

app
.use(bodyParser.json())
.post('/notFound', notFound)
.post('/onlyAdmin', addUserType, onlyAdmin, notFound)
.post('/notOnlyMember', addUserType, notOnlyMember, notFound)

function addUserType (req, res, next) {
  req.user = { type: req.body.type }
  next()
}

function test (funcStr, args) {
  return request(app)
          .post('/'+funcStr)
          .send({ type: args })
}

describe('middleware', function () {
  describe('notFound', function () {
    it('should return not found', function (done) {
      test('notFound')
        .expect(404, done)
    })
  })

  describe('onlyAdmin', function () {
    it('should access', function (done) {
      test('onlyAdmin', 'admin')
        .expect(404, done)
    })
    it('should not access', function (done) {
      test('onlyAdmin', 'member')
        .expect(401, done)
    })
  })

  describe('notOnlyMember', function () {
    it('should access', function (done) {
      test('notOnlyMember', 'admin')
        .expect(404, done)
    })
    it('should access', function (done) {
      test('notOnlyMember', 'owner')
        .expect(404, done)
    })
    it('should not access', function (done) {
      test('notOnlyMember', 'member')
        .expect(401, done)
    })
  })
})
```

# End-to-end testing
After testing all functions in the `common` folder, it's up to you to continue end-to-end testing in the `mocha` framework or use other tools.

Feel free to leave a comment if you have any questions.