Wednesday, December 18, 2019

Super-test with mocking middleware In Jest

So, today we are going to see how do we mock the middleware if it is applied over the Express server.

So, here is our agenda:
  1. Create an express app
  2. Add a middleware to Authenticate the user
  3. Test the app using Jest (integration test NOT UNIT)
index.js

1
2
3
const server = require('./server')
const PORT = process.env.PORT || 3000
server.listen(PORT, () => console.log(`Server is live at localhost:${PORT}`))

server.js
1
2
3
4
5
6
7
const express = require('express')
const middleware = require('./middleware')
const welcomeRoute = require('./welcome-route')
const server = express()
server.use(express.json())
server.use('/api', middleware, welcomeRoute)
module.exports = server


is-user-authentic.js
1
2
3
4
5
const faker = require('faker/locale/en_US')
const isUserAuthentic = () => faker.random.arrayElement([true, false])
module.exports = {
  isUserAuthentic
}

middleware.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const { isUserAuthentic } = require('./is-user-authentic')

const middleware = (req, res, next) => { 
  if (!isUserAuthentic()) {
    res.sendStatus(401)
    return next(new Error('Unauthenticated user'))
  }
  next()
}
const myExport = module.exports = middleware

welcome-route.js
1
2
3
4
5
const { Router } = require('express')
const WelcomeService = require('./welcome-service')
const router = Router()
router.get('/', (_req, res) => res.json({ 'message': WelcomeService.message() }))
module.exports = router

welcome-service.js
1
2
3
module.exports = {
  message: () => 'hello world'
}

In this code, the flow is like:
  1. Start the server at 3000 (line 3, index.js)
  2. Middleware is applied to base path (line 6, server.js)
  3. If you hit http://localhost:3000/api, is-user-authentic will return true/false randomly which will return either return 'hello world' or 401 unauthorised
Now in order to test this, we will use jest mocking.

routes.test.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
const request = require('supertest')
const faker = require('faker/locale/en_US')

let app
let WelcomeServiceSpyOfMessage
let IsUserAuthenticSpyOnIsUserAuthentic

describe('test', () => {
  beforeEach(() => {

    /**
     * Always create spy then create server
     */
    const WelcomeService = require('../src/welcome-service')
    WelcomeServiceSpyOfMessage = jest.spyOn(
      WelcomeService,
      'message',
    )
    
    /**
     * Always create spy then create server
     */
    const IsUserAuthentic = require('../src/is-user-authentic')
    IsUserAuthenticSpyOnIsUserAuthentic = jest.spyOn(
      IsUserAuthentic,
      'isUserAuthentic'
    )
    app = require('../src/server')
  })

  afterEach(() => {
    /**
     * Most important since b'coz of caching, the mocked implementations sometimes does not resets 
     */
    jest.resetModules()
    jest.restoreAllMocks()
  })

  it('1. Mock implementation, with successful user auth [isUserAuthentic as true]', async () => {
    const mockedMessage = faker.lorem.sentence()
    WelcomeServiceSpyOfMessage.mockImplementation(() => mockedMessage)
    // --> For successful user authentication, sending true :
    IsUserAuthenticSpyOnIsUserAuthentic.mockImplementation(() => true)
    const result = await request(app)
      .get('/api')
    expect(result.statusCode).toBe(200)
    expect(result.body).toHaveProperty('message', mockedMessage)
  })

  it('2. After restored implementation, with successful user auth [isUserAuthentic as true]', async () => {
    /**
     * This will return { 'message' : 'hello world'},
     * since we restored the mock implementation, in afterEach
     * restoreAllMocks and resetModules,
     * works only for methods those are mocked
     * with .spyOn()
     */
    // --> For successful user authentication, sending back true
    IsUserAuthenticSpyOnIsUserAuthentic.mockImplementation(() => true)
    const result = await request(app)
      .get('/api')
    expect(result.statusCode).toBe(200)
    /**
     * This expectation is the same returning from the service
     */
    expect(result.body).toHaveProperty('message', 'hello world')
  })

  it('3. Returning user is Unauthorized, [isUserAuthentic as false]', async () => {
    // for Unauthorized, we return false
    IsUserAuthenticSpyOnIsUserAuthentic.mockImplementation(() => false)
    const result = await request(app)
      .get('/api')
    expect(result.statusCode).toBe(401)
  })
})

In this test there are certain things that needs to be taken care before loading the express server.

  1. Mock the middleware before you load the server otherwise if you mock middleware after the server is loaded new reference of the object will be mocked and even after the mock the original implementation will be called.
  2. In this test case, we loaded the server at line 28, but the spy was already hooked to the functions before that only.
  3. In afterEach we will reset modules and reset mocks to make sure mocked implementation of one test will not interfere in other tests.
  4. Now every time module is loaded then spy is added and mock will be hooked.
The source code for the above example is available at https://github.com/ankur20us/demo-jest-supertest

Happy Coding
:) 

No comments:

Post a Comment