Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Testing serverless apps

Testing serverless apps

Talk from HolyJS 2017 conference in Moscow.
https://holyjs-moscow.ru/talks/5hfhepewii0iesi46gmgou/

Slobodan Stojanović

December 10, 2017
Tweet

More Decks by Slobodan Stojanović

Other Decks in Programming

Transcript

  1. !TMPCPEBO@ function factorial(n) { if (typeof n !== 'number') {

    throw new TypeError() } if (n === 0) { return 1 } return n * factorial(n - 1) }
  2. !TMPCPEBO@ describe('Factorial', () => { it('should throw an error if

    it receives anything but number', () => { expect(() => factorial()).toThrow() expect(() => factorial('A')).toThrow() expect(() => factorial([])).toThrow() }) it('should return 1 if 0 is passed', () => { expect(factorial(0)).toBe(1) }) it('should work :D', () => { expect(factorial(5)).toBe(120) }) })
  3. !TMPCPEBO@ . ├── api.js ├── config │ └── env.json ├──

    data │ └── pizzas.json ├── handlers │ ├── create-order.js │ ├── delete-order.js │ ├── get-pizzas.js │ └── … ├── node_modules ├── package.json └── spec ├── handlers │ └── … └── support ├── jasmine-runner.js └── jasmine.json
  4. !TMPCPEBO@ /spec/support/jasmine-runner.js /* global jasmine */ 'use strict' const Jasmine

    = require('jasmine') const SpecReporter = require('jasmine-spec-reporter').SpecReporter const jrunner = new Jasmine() let filter process.argv.slice(2).forEach(option => { if (option === 'full') { // Remove default reporter logs jrunner.configureDefaultReporter({ print() {} }) // Add jasmine-spec-reporter jasmine.getEnv().addReporter(new SpecReporter()) } if (option.match('^filter=')) filter = option.match('^filter=(.*)')[1] }) // Load configuration from jasmine.json jrunner.loadConfigFile() jrunner.execute(undefined, filter)
  5. !TMPCPEBO@ /spec/support/jasmine-runner.js /* global jasmine */ 'use strict' const Jasmine

    = require('jasmine') const SpecReporter = require('jasmine-spec-reporter').SpecReporter const jrunner = new Jasmine() let filter process.argv.slice(2).forEach(option => { if (option === 'full') { // Remove default reporter logs jrunner.configureDefaultReporter({ print() {} }) // Add jasmine-spec-reporter jasmine.getEnv().addReporter(new SpecReporter()) } if (option.match('^filter=')) filter = option.match('^filter=(.*)')[1] }) // Load configuration from jasmine.json jrunner.loadConfigFile() jrunner.execute(undefined, filter)
  6. !TMPCPEBO@ /spec/support/jasmine-runner.js /* global jasmine */ 'use strict' const Jasmine

    = require('jasmine') const SpecReporter = require('jasmine-spec-reporter').SpecReporter const jrunner = new Jasmine() let filter process.argv.slice(2).forEach(option => { if (option === 'full') { // Remove default reporter logs jrunner.configureDefaultReporter({ print() {} }) // Add jasmine-spec-reporter jasmine.getEnv().addReporter(new SpecReporter()) } if (option.match('^filter=')) filter = option.match('^filter=(.*)')[1] }) // Load configuration from jasmine.json jrunner.loadConfigFile() jrunner.execute(undefined, filter)
  7. !TMPCPEBO@ /spec/support/jasmine-runner.js /* global jasmine */ 'use strict' const Jasmine

    = require('jasmine') const SpecReporter = require('jasmine-spec-reporter').SpecReporter const jrunner = new Jasmine() let filter process.argv.slice(2).forEach(option => { if (option === 'full') { // Remove default reporter logs jrunner.configureDefaultReporter({ print() {} }) // Add jasmine-spec-reporter jasmine.getEnv().addReporter(new SpecReporter()) } if (option.match('^filter=')) filter = option.match('^filter=(.*)')[1] }) // Load configuration from jasmine.json jrunner.loadConfigFile() jrunner.execute(undefined, filter)
  8. !TMPCPEBO@ /spec/support/jasmine-runner.js /* global jasmine */ 'use strict' const Jasmine

    = require('jasmine') const SpecReporter = require('jasmine-spec-reporter').SpecReporter const jrunner = new Jasmine() let filter process.argv.slice(2).forEach(option => { if (option === 'full') { // Remove default reporter logs jrunner.configureDefaultReporter({ print() {} }) // Add jasmine-spec-reporter jasmine.getEnv().addReporter(new SpecReporter()) } if (option.match('^filter=')) filter = option.match('^filter=(.*)')[1] }) // Load configuration from jasmine.json jrunner.loadConfigFile() jrunner.execute(undefined, filter)
  9. !TMPCPEBO@ /spec/support/jasmine-runner.js /* global jasmine */ 'use strict' const Jasmine

    = require('jasmine') const SpecReporter = require('jasmine-spec-reporter').SpecReporter const jrunner = new Jasmine() let filter process.argv.slice(2).forEach(option => { if (option === 'full') { // Remove default reporter logs jrunner.configureDefaultReporter({ print() {} }) // Add jasmine-spec-reporter jasmine.getEnv().addReporter(new SpecReporter()) } if (option.match('^filter=')) filter = option.match('^filter=(.*)')[1] }) // Load configuration from jasmine.json jrunner.loadConfigFile() jrunner.execute(undefined, filter)
  10. !TMPCPEBO@ 'use strict' const listOfPizzas = require('../data/pizzas.json') function getPizzas(pizzaId) {

    if (!pizzaId) return listOfPizzas const pizza = listOfPizzas.find((pizza) => { return pizza.id == pizzaId }) if (pizza) return pizza throw `The pizza you requested was not found` } module.exports = getPizzas /handlers/get-pizzas.js
  11. !TMPCPEBO@ 'use strict' const listOfPizzas = require('../data/pizzas.json') function getPizzas(pizzaId) {

    if (!pizzaId) return pizzas const pizza = pizzas.find((pizza) => { return pizza.id == pizzaId }) if (pizza) return pizza throw `The pizza you requested was not found` } module.exports = getPizzas /handlers/get-pizzas.js
  12. !TMPCPEBO@ 'use strict' const listOfPizzas = require('../data/pizzas.json') function getPizzas(pizzaId, pizzas)

    { pizzas = pizzas || listOfPizzas if (!pizzaId) return pizzas const pizza = pizzas.find((pizza) => { return pizza.id == pizzaId }) if (pizza) return pizza throw `The pizza you requested was not found` } module.exports = getPizzas /handlers/get-pizzas.js
  13. !TMPCPEBO@ 'use strict' const listOfPizzas = require('../data/pizzas.json') function getPizzas(pizzaId, pizzas)

    { pizzas = pizzas || listOfPizzas if (!pizzaId) return pizzas const pizza = pizzas.find((pizza) => { return pizza.id == pizzaId }) if (pizza) return pizza throw `The pizza you requested was not found` } module.exports = getPizzas /handlers/get-pizzas.js
  14. !TMPCPEBO@ 'use strict' const listOfPizzas = require('../data/pizzas.json') function getPizzas(pizzaId, pizzas)

    { pizzas = pizzas || listOfPizzas if (!pizzaId) return pizzas const pizza = pizzas.find((pizza) => { return pizza.id == pizzaId }) if (pizza) return pizza throw `The pizza you requested was not found` } module.exports = getPizzas /handlers/get-pizzas.js
  15. !TMPCPEBO@ 'use strict' const listOfPizzas = require('../data/pizzas.json') function getPizzas(pizzaId, pizzas)

    { pizzas = pizzas || listOfPizzas if (!pizzaId) return pizzas const pizza = pizzas.find((pizza) => { return pizza.id == pizzaId }) if (pizza) return pizza throw `The pizza you requested was not found` } module.exports = getPizzas /handlers/get-pizzas.js
  16. !TMPCPEBO@ /spec/handlers/get-pizzas.js /* global describe, it, expect */ 'use strict'

    const underTest = require('../../handlers/get-pizzas') const pizzas = [{ id: 1, name: 'Capricciosa' }] describe('Get pizzas handler', () => { it('should return the list of all pizzas', () => { expect(underTest(undefined, pizzas)).toEqual(pizzas) }) it('should return the pizza with the given ID', () => { expect(underTest(1, pizzas)).toEqual(pizzas[0]) }) it('should throw an error if non-existing ID is passed', () => { expect(() => underTest(42, pizzas)).toThrow('...') }) })
  17. !TMPCPEBO@ /spec/handlers/get-pizzas.js /* global describe, it, expect */ 'use strict'

    const underTest = require('../../handlers/get-pizzas') const pizzas = [{ id: 1, name: 'Capricciosa' }] describe('Get pizzas handler', () => { it('should return the list of all pizzas', () => { expect(underTest(undefined, pizzas)).toEqual(pizzas) }) it('should return the pizza with the given ID', () => { expect(underTest(1, pizzas)).toEqual(pizzas[0]) }) it('should throw an error if non-existing ID is passed', () => { expect(() => underTest(42, pizzas)).toThrow('...') }) })
  18. !TMPCPEBO@ /spec/handlers/get-pizzas.js /* global describe, it, expect */ 'use strict'

    const underTest = require('../../handlers/get-pizzas') const pizzas = [{ id: 1, name: 'Capricciosa' }] describe('Get pizzas handler', () => { it('should return the list of all pizzas', () => { expect(underTest(undefined, pizzas)).toEqual(pizzas) }) it('should return the pizza with the given ID', () => { expect(underTest(1, pizzas)).toEqual(pizzas[0]) }) it('should throw an error if non-existing ID is passed', () => { expect(() => underTest(42, pizzas)).toThrow('...') }) })
  19. !TMPCPEBO@ /spec/handlers/get-pizzas.js /* global describe, it, expect */ 'use strict'

    const underTest = require('../../handlers/get-pizzas') const pizzas = [{ id: 1, name: 'Capricciosa' }] describe('Get pizzas handler', () => { it('should return the list of all pizzas', () => { expect(underTest(undefined, pizzas)).toEqual(pizzas) }) it('should return the pizza with the given ID', () => { expect(underTest(1, pizzas)).toEqual(pizzas[0]) }) it('should throw an error if non-existing ID is passed', () => { expect(() => underTest(42, pizzas)).toThrow('...') }) })
  20. !TMPCPEBO@ /spec/handlers/get-pizzas.js /* global describe, it, expect */ 'use strict'

    const underTest = require('../../handlers/get-pizzas') const pizzas = [{ id: 1, name: 'Capricciosa' }] describe('Get pizzas handler', () => { it('should return the list of all pizzas', () => { expect(underTest(undefined, pizzas)).toEqual(pizzas) }) it('should return the pizza with the given ID', () => { expect(underTest(1, pizzas)).toEqual(pizzas[0]) }) it('should throw an error if non-existing ID is passed', () => { expect(() => underTest(42, pizzas)).toThrow('...') }) })
  21. !TMPCPEBO@ /spec/handlers/get-pizzas.js /* global describe, it, expect */ 'use strict'

    const underTest = require('../../handlers/get-pizzas') const pizzas = [{ id: 1, name: 'Capricciosa' }] describe('Get pizzas handler', () => { it('should return the list of all pizzas', () => { expect(underTest(undefined, pizzas)).toEqual(pizzas) }) it('should return the pizza with the given ID', () => { expect(underTest(1, pizzas)).toEqual(pizzas[0]) }) it('should throw an error if non-existing ID is passed', () => { expect(() => underTest(42, pizzas)).toThrow('...') }) })
  22. !TMPCPEBO@ 'use strict' const AWS = require('aws-sdk') const docClient =

    new AWS.DynamoDB.DocumentClient() const rp = require('minimal-request-promise') /handlers/create-order.js
  23. !TMPCPEBO@ function createOrder(request) { const userData = request.context.authorizer.claims let userAddress

    = request.body && request.body.address if (!userAddress) { userAddress = JSON.parse(userData.address).formatted } if (!request.body || !request.body.pizza || userAddress) throw new Error('To order pizza please provide pizza type and address where pizza should be delivered')
  24. !TMPCPEBO@ return rp.post( 'https://some-like-it-hot-api.effortlessserverless.com/delivery', { headers: { Authorization: process.env.deliveryToken, 'Content-type':

    'application/json' }, body: JSON.stringify({ pickupTime: new Date(), pickupAddress: 'Aunt Maria Pizzeria', deliveryAddress: userAddress, webhookUrl: 'https://g8fhlgccof.execute-api.eu- central-1.amazonaws.com/latest/delivery', }) }) .then(rawResponse => JSON.parse(rawResponse.body))
  25. !TMPCPEBO@ .then(response => { return docClient.put({ TableName: 'pizza-orders', Item: {

    cognitoUsername: userAddress['cognito:username'], orderId: response.deliveryId, pizza: request.body.pizza, address: userAddress, orderStatus: 'pending' } }).promise() }) } module.exports = createOrder
  26. !TMPCPEBO@ /spec/helpers/fake-https-request.js /*global beforeEach, afterEach */ 'use strict' const fake

    = require('fake-http-request') beforeEach(() => fake.install('https')) afterEach(() => fake.uninstall('https'))
  27. !TMPCPEBO@ /* global jasmine, describe, beforeAll, afterAll, it, expect */

    'use strict' const underTest = require('../../handlers/create-order') const AWS = require('aws-sdk') const dynamoDb = new AWS.DynamoDB({ apiVersion: '2012-08-10', region: 'eu-central-1' }) const https = require('https') const tableName = `pizzaOrderTest${new Date().getTime()}` jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 describe('Create order integration', () => { /* ... */ }) /spec/handlers/create-order-integration-test.js
  28. !TMPCPEBO@ /* global jasmine, describe, beforeAll, afterAll, it, expect */

    'use strict' const underTest = require('../../handlers/create-order') const AWS = require('aws-sdk') const dynamoDb = new AWS.DynamoDB({ apiVersion: '2012-08-10', region: 'eu-central-1' }) const https = require('https') const tableName = `pizzaOrderTest${new Date().getTime()}` jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 describe('Create order integration', () => { /* ... */ }) /spec/handlers/create-order-integration-test.js
  29. !TMPCPEBO@ /* global jasmine, describe, beforeAll, afterAll, it, expect */

    'use strict' const underTest = require('../../handlers/create-order') const AWS = require('aws-sdk') const dynamoDb = new AWS.DynamoDB({ apiVersion: '2012-08-10', region: 'eu-central-1' }) const https = require('https') const tableName = `pizzaOrderTest${new Date().getTime()}` jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 describe('Create order integration', () => { /* ... */ }) /spec/handlers/create-order-integration-test.js
  30. !TMPCPEBO@ beforeAll(done => { // Test table params const params

    = { AttributeDefinitions: [{ AttributeName: 'orderId', AttributeType: 'S' }], KeySchema: [{ AttributeName: 'orderId', KeyType: 'HASH' }], TableName: tableName } // Create test table dynamoDb.createTable(params).promise() .then(() => dynamoDb.waitFor('tableExists', { TableName: tableName }).promise()) .then(done) .catch(done.fail) }) /spec/handlers/create-order-integration-test.js
  31. !TMPCPEBO@ afterAll(done => { // Destroy test table dynamoDb.deleteTable({ TableName:

    tableName }).promise() .then(() => dynamoDb.waitFor('tableNotExists', { TableName: tableName }).promise()) .then(done) .catch(done.fail) }) /spec/handlers/create-order-integration-test.js
  32. !TMPCPEBO@ it('should throw an error if request body is not

    valid', () => { expect(() => underTest()).toThrow() expect(() => underTest({ body: { pizza: 1 }, context: { authorizer: { claims: { address: '{}' }}} })).toThrow() expect(() => underTest({ context: { authorizer: { claims: { address: '{"formatted":"221b Baker Street"}' } } } })).toThrow() }) /spec/handlers/create-order-integration-test.js
  33. !TMPCPEBO@ it('should contact Some Like It Hot delivery API', done

    => { underTest({ body: { pizza: 1, address: '221b Baker Street' }, context: { authorizer: { claims: { address: '{"formatted":"some address"}' } } } }) /spec/handlers/create-order-integration-test.js
  34. !TMPCPEBO@ https.request.pipe(callOptions => { expect(callOptions).toEqual(jasmine.objectContaining({ method: 'POST', hostname: 'some-like-it-hot-api.effortlessserverless.com', path:

    '/delivery', protocol: 'https:', headers: { Authorization: 'aunt-marias-pizzeria-1234567890', 'Content-type': 'application/json' }, body: JSON.stringify({ pickupAddress: 'Aunt Maria Pizzeria', deliveryAddress: '221b Baker Street' }) })) done() }) }) /spec/handlers/create-order-integration-test.js
  35. !TMPCPEBO@ • Serverless apps testing works similar to non-serverless apps

    • Serverless decreases the price and speed up the test execution • Focus on business logic, also try testing the parts that are important and critical for your app • Most important, try to understand business requirements before coding and testing • Try serverless, it can make your life easier