Node.js Meetup Berlin 17 October 2017 @robinpokorny Async testing KOA with JEST
A presentation at Node.js Berlin Meetup in October 2017 in Berlin, Germany by Robin Pokorny
Node.js Meetup Berlin 17 October 2017 @robinpokorny Async testing KOA with JEST
Node.js Meetup Berlin 17 October 2017 @robinpokorny INFO Slides accompany a talk. Here, the talk is missing. I wrote a transcript which can substitute the talk. Async testing KOA Find it on this link: with JEST bit.ly/jest-koa
WHAT IS KOA next generation web framework Express’ spiritual successor using ES2017 async/await (no callback hell, yay!) http://koajs.com/
WHAT IS JEST delightful, zero configuration testing platform Jasmine’s and Expect’s (spiritual) successor first-class mocking, snapshots, async testing http://facebook.github.io/jest/
1 2 Testing MIDDLEWARE Testing API
1 2 Testing MIDDLEWARE Testing API
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } const app = new Koa() app.use(greetings) app.listen(3000)
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } const app = new Koa() app.use(greetings) app.listen(3000)
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } const app = new Koa() app.use(greetings) app.listen(3000)
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } const app = new Koa() app.use(greetings) app.listen(3000)
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } const app = new Koa() app.use(greetings) app.listen(3000)
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } test(‘greetings works’, async () !=> { const ctx = {} await greetings(ctx, () !=> {}) expect(ctx.body).toBe( ‘Hello. Remember to subscribe.’ ) }) SIMPLE TEST
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } test(‘greetings works’, async () !=> { const ctx = {} await greetings(ctx, () !=> {}) expect(ctx.body).toBe( ‘Hello. Remember to subscribe.’ ) }) SIMPLE TEST
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } test(‘greetings works’, async () !=> { const ctx = {} await greetings(ctx, () !=> {}) expect(ctx.body).toBe( ‘Hello. Remember to subscribe.’ ) }) SIMPLE TEST
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } test(‘greetings works’, async () !=> { const ctx = {} await greetings(ctx, () !=> {}) expect(ctx.body).toBe( ‘Hello. Remember to subscribe.’ ) }) SIMPLE TEST
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } test(‘greetings works’, async () !=> { const ctx = {} await greetings(ctx, () !=> {}) expect(ctx.body).toBe( ‘Hello. Remember to subscribe.’ ) }) SIMPLE TEST
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } test(‘greetings works’, async () !=> { const ctx = {} await greetings(ctx, () !=> {}) expect(ctx.body).toBe( ‘Hello. Remember to subscribe.’ ) }) SIMPLE TEST
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } test(‘greetings works’, async () !=> { const ctx = {} await greetings(ctx, () !=> {}) expect(ctx.body).toBe( ‘Hello. Remember to subscribe.’ ) }) SIMPLE TEST
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } test(‘greetings works in order’, async () !=> { const ctx = {} const next = jest.fn(() !=> { expect(ctx.body).toBe(‘Hello.’) ctx.body += ’ I am content.’ }) await greetings(ctx, next) expect(next).toHaveBeenCalledTimes(1) expect(ctx.body).toBe( ‘Hello. I am content. Remember to subscribe.’ ) }) BEFOREANDAFTER TEST
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } test(‘greetings works in order’, async () !=> { const ctx = {} const next = jest.fn(() !=> { expect(ctx.body).toBe(‘Hello.’) ctx.body += ’ I am content.’ }) ← before await greetings(ctx, next) expect(next).toHaveBeenCalledTimes(1) expect(ctx.body).toBe( ‘Hello. I am content. Remember to subscribe.’ ) }) BEFOREANDAFTER TEST
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } test(‘greetings works in order’, async () !=> { const ctx = {} const next = jest.fn(() !=> { expect(ctx.body).toBe(‘Hello.’) ctx.body += ’ I am content.’ }) ← before await greetings(ctx, next) expect(next).toHaveBeenCalledTimes(1) expect(ctx.body).toBe( ← after ‘Hello. I am content. Remember to subscribe.’ ) }) BEFOREANDAFTER TEST
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } test(‘greetings works in order’, async () !=> { const ctx = {} const next = jest.fn(() !=> { expect(ctx.body).toBe(‘Hello.’) ctx.body += ’ I am content.’ }) ← before await greetings(ctx, next) expect(next).toHaveBeenCalledTimes(1) expect(ctx.body).toBe( ← after ‘Hello. I am content. Remember to subscribe.’ ) }) BEFOREANDAFTER TEST
const greetings = async (ctx, next) !=> { ctx.body = ‘Hello.’ await next() ctx.body += ’ Remember to subscribe.’ } test(‘greetings works in order’, async () !=> { const ctx = {} const next = jest.fn(() !=> { expect(ctx.body).toBe(‘Hello.’) ctx.body += ’ I am content.’ }) ← before await greetings(ctx, next) expect(next).toHaveBeenCalledTimes(1) expect(ctx.body).toBe( ← after ‘Hello. I am content. Remember to subscribe.’ ) }) BEFOREANDAFTER TEST
test(‘greetings works complete’, async () !=> { const ctx = { response: { set: jest.fn() } !/* ADD OTHER MOCKS !*/ } COMPLETE TEST const next = jest.fn(() !=> { expect(ctx).toMatchSnapshot() }) await expect(greetings(ctx, next)) .resolves.toBeUndefined() expect(next).toHaveBeenCalledTimes(1) expect(ctx).toMatchSnapshot() expect(ctx.response.set.mock.calls).toMatchSnapshot() })
test(‘greetings works complete’, async () !=> { const ctx = { response: { set: jest.fn() } !/* ADD OTHER MOCKS !*/ } COMPLETE TEST const next = jest.fn(() !=> { expect(ctx).toMatchSnapshot() }) await expect(greetings(ctx, next)) .resolves.toBeUndefined() expect(next).toHaveBeenCalledTimes(1) expect(ctx).toMatchSnapshot() expect(ctx.response.set.mock.calls).toMatchSnapshot() })
// Jest Snapshot v1, https://goo.gl/fbAQLP exports[greetings works complete 1
] = Object { "body": “Hello.", "response": Object { "set": [Function], }, }
; exports[greetings works complete 2
] = …
;
SNAPSHOT
test(‘greetings works complete’, async () !=> { const ctx = { response: { set: jest.fn() } !/* ADD OTHER MOCKS !*/ } COMPLETE TEST const next = jest.fn(() !=> { expect(ctx).toMatchSnapshot() }) await expect(greetings(ctx, next)) .resolves.toBeUndefined() expect(next).toHaveBeenCalledTimes(1) expect(ctx).toMatchSnapshot() expect(ctx.response.set.mock.calls).toMatchSnapshot() })
SNAPSHOT // Jest Snapshot v1, https://goo.gl/fbAQLP exports[greetings works complete 3
] = Array [ Array [ "Etag", 1234, ], ]
;
test(‘greetings works complete’, async () !=> { const ctx = { response: { set: jest.fn() } !/* ADD OTHER MOCKS !*/ } COMPLETE TEST const next = jest.fn(() !=> { expect(ctx).toMatchSnapshot() }) await expect(greetings(ctx, next)) .resolves.toBeUndefined() expect(next).toHaveBeenCalledTimes(1) expect(ctx).toMatchSnapshot() expect(ctx.response.set.mock.calls).toMatchSnapshot() })
.RESOLVES & .REJECTS Better error messages More errors Readable and short
Read error ✖ Expected received Promise to resolve, instead it rejected to value [Error: Read error]
by @kentcdodds
by @kentcdodds
1 2 Testing MIDDLEWARE Testing API
MORE THAN SUM App ≠ compose(app.middleware) Koa wraps the native response and request API testing, HTTP assertions
SUPERTEST HTTP assertions library wrapper over SuperAgent support for Promises https://github.com/visionmedia/supertest
A CLEAR AND CONCISE INTRODUCTION TO TESTING KOA WITH JEST AND SUPERTEST Valentino Gagliardi https://www.valentinog.com/blog/testing-api-koa-jest/
!// server/index.js const app = new Koa() const router = new Router() SAMPLE APP router.get(‘/’, async ctx !=> { ctx.body = { data: ‘Sending some JSON’, person: { name: ‘Ferdinand’, lastname: ‘Vaněk’, role: ‘Brewery worker’, age: 42 } } }) app.use(router.routes()) module.exports = app
app.listen(3000) ✖ app.callback() creates server Supertest will open and close need to close after each test the server for us.
!// test/root.spec.js TEST BOILERPLATE const request = require(‘supertest’) const app = require(‘!../server’) test(‘root route’, async () !=> { const response = await request(app.callback()).get(‘/’); expect(response).toBeDefined() !// @TODO })
!// test/root.spec.js TEST BOILERPLATE const request = require(‘supertest’) const app = require(‘!../server’) test(‘root route’, async () !=> { const response = await request(app.callback()).get(‘/’); expect(response).toBeDefined() !// @TODO })
!// test/root.spec.js TEST BOILERPLATE const request = require(‘supertest’) const app = require(‘!../server’) test(‘root route’, async () !=> { const response = await request(app.callback()).get(‘/’); expect(response).toBeDefined() !// @TODO })
ITEM-LEVEL ASSERTIONS 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’]) )
ITEM-LEVEL ASSERTIONS 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’]) )
expect(response.body).toEqual( expect.objectContaining({ person: { name: expect.anything(), lastname: expect.any(String), role: expect.stringMatching(/^Brewery/), age: expect.any(Number) } }) ) OBJECT EQUALITY
expect(response.body).toEqual( expect.objectContaining({ person: { name: expect.anything(), lastname: expect.any(String), role: expect.stringMatching(/^Brewery/), age: expect.any(Number) } }) ) expect.objectContaining({ x: 1 }) ✖ { x: 1 } OBJECT EQUALITY
expect(response.body).toMatchSnapshot()
SNAPSHOTS
!// test/!snapshots!/root.spec.js.snap exports[root route with object equality 1
] = Object { "data": "Sending some JSON", "person": Object { "age": 42, "lastname": "Vaněk", "name": "Ferdinand", "role": "Brewery worker", }, }
;
TDD ✖ SNAPSHOTS algorithms structures write before concurrent or after part whole
NOT ONLY KOA applies to other frameworks API testing - no change convenient for refactoring
RELATED • • A clear and concise introduction to testing Koa with Jest and Supertest An Introduction to Building TDD RESTful APIs with Koa 2, Mocha and Chai • • • • • • both by Valentino Gagliardi API testing with Jest by Koen van Gilst Testing async/await middleware? (GitHub Issue) Async testing in Jest (recording of presentation) Snapshot Testing APIs with Jest by Dave Ceddia Snapshot testing in Jest (recording of presentation)
ARTICLE @robinpokorny bit.ly/jest-koa