Real World Fixtures

Real World Fixtures

Using http mocks during development of ember applications. Using http mocks allow you to keep the same adapters you're used to using, predict app behavior, ensure proper handling of ajax errors, and even drive the API spec for your app. They are extremely helpful for building out the front-end before moving on to implementing the API.

6254dc2b7e4f26b2ab5d05c560834671?s=128

Chris Ball

August 14, 2014
Tweet

Transcript

  1. Real World Fixtures Using a mock API server during development

  2. Real World Fixtures Using a mock API server during development

    API stubs
  3. Real World Fixtures Using a mock API server during development

    API stubs
  4. Real World Fixtures Using a mock API server during development

    API stubs HTTP Mocks
  5. Chris Ball @cball_

  6. thanks to this guy

  7. What we’ll talk about: Mock Server? HTTP Mock? Who can

    benefit Why mocks are better than fixtures Walkthrough developing a feature How to make your own ☼ ☼ ☼ ☼ ☼ ☼
  8. Mock Server? HTTP Mock?

  9. Mock Server Added as middleware or runs in a separate

    process. ☼
  10. Mock Server Intercepts request and sends back a predetermined response.

  11. Mock Server Often used for testing external services (tests still

    work on a plane!)
  12. Mock Server ember-cli has this built in as of v0.0.28

  13. HTTP Mock Fake API response used by mock server ☼

  14. Can be hardcoded or filled dynamically with something like Faker.

    ☼ HTTP Mock
  15. Tell a mock server to respond with an HTTP mock

    and you have a fake API Mock Server + HTTP Mock ☼
  16. sinon.js pretender ☼ Mock Server + HTTP Mock

  17. this.server = sinon.fakeServer.create(); this.server.respondWith("GET", “/comments.json”, [200, { "Content-Type": "application/json" },

    '[{ "id": 12, "comment": "Hey there" }]’ ]); ! this.server.autoRespond = true; this.server.restore(); sinon.js
  18. var server = new Pretender(function(){ this.get('/photos', function(request){ var all =

    JSON.stringify( Object.keys(PHOTOS).map(function(k){return PHOTOS[k]}) ); return [200, {"Content-Type": "application/json"}, all] }); }); ! server.shutdown() pretender
  19. Mock Server - intercepts request and responds - ember-cli has

    one built in HTTP Mock - fake API response used by server ☼ ☼ Mock Server + HTTP Mock
  20. Who can benefit from using HTTP Mocks?

  21. Solo Developer Avoid context switching between apps. ☼

  22. Front-end Team Don’t wait for your API to be built.

  23. Ember Data Learner Discover how to properly structure responses. ☼

  24. ☼ Who can benefit? • Solo Developer • Front-end Team

    • Ember Data Learner • Everyone!
  25. Why HTTP Mocks are better than fixtures.

  26. Fixtures? In-memory representation of objects used by the data store.

  27. Keep the same adapters HTTP Mocks will allow you to

    use the same adapters as the rest of your app. ☼
  28. RESTAdapter Expects api will return json in camelCase.

  29. { "person": { "firstName": “Devin", "lastName": "Townsend", "occupation": "Musician" }

    }
  30. ActiveModelAdapter Expects api will return json in snake_case.

  31. { "person": { "first_name": "Devin", "last_name": "Townsend", "occupation": "Musician" }

    }
  32. Fixture Adapter Expects flat object, keys map directly to attribute

    names.
  33. { "firstName": "Devin", "lastName": "Townsend", "occupation": "Musician" }

  34. Why this helps “Use this as a mock response. The

    final API will look almost identical.”
  35. { "person": { "first_name": "Devin", "last_name": "Townsend", "occupation": “Musician”, “image_ids”:

    [1] }, "images": { "id": 1 "url": "http://myimage.gif" } }
  36. If using a fixture Here’s what your model would look

    like:
  37. App.Person = DS.Model.extend({ first_name: DS.attr('string'), last_name: DS.attr('string'), occupation: DS.attr('string') });

  38. App.Person = DS.Model.extend({ first_name: DS.attr('string'), last_name: DS.attr('string'), occupation: DS.attr('string') });

    DOH.
  39. Fixtures root json Our API response also has an outer

    “person” wrapper.
  40. Predict app behavior Especially important for non- standard API’s (sideloading,

    serializers) ☼
  41. Make ajax requests Example: build out behavior for an ajax

    error event ☼
  42. Fixtures always resolve screenshot of code

  43. Drive the API spec A lot of hard API decisions

    can be made for you ☼
  44. jsonapi.org Great reference for API design.

  45. Steps from mock live (almost always) ☼

  46. Who doesn't love this? pretend github screenshot with lots of

    delete
  47. ☼ HTTP Mocks are better • Keep the same adapters

    • Predict app behavior • Make ajax requests • Drive the API spec • Moving to live API? Trash your stubs
  48. Let’s walk through development of a new feature.

  49. Background We’re building a concert ticketing system

  50. Venue Band Concert Ticket export default DS.Model.extend({ name: DS.attr(‘string’), address:

    DS.belongsTo(‘address’), openDate: DS.attr(‘date’), closeDate: DS.attr(‘date’) }); export default DS.Model.extend({ name: DS.attr(‘string’), members: DS.belongsTo(‘member’), }); export default DS.Model.extend({ date: DS.attr(‘date’), bands: DS.hasMany(‘band’), venue: DS.belongsTo(‘venue’) }); export default DS.Model.extend({ seat: DS.attr(‘string’), concert: DS.belongsTo(‘concert’) });
  51. Venue export default DS.Model.extend({ name: DS.attr(‘string’), address: DS.belongsTo(‘address’), openDate: DS.attr(‘date’),

    closeDate: DS.attr(‘date’) }); (this exists in our app already)
  52. Build out Concert export default DS.Model.extend({ date: DS.attr(‘date’), bands: DS.hasMany(‘band’),

    venue: DS.belongsTo(‘venue’) });
  53. Create an adapter ☼ Set the host to ember app

  54. # app/adapters/concert.js import DS from ‘ember-data’; ! export default DS.ActiveModelAdapter.extend({

    host: ‘http://localhost:4200' });
  55. Add HTTP Mock ☼

  56. > ember g http-mock concert

  57. # server/index.js var bodyParser = require('body-parser'); var globSync = require('glob').sync;

    var routes = globSync('./routes/**/*.js', { cwd: __dirname }).map(require); ! module.exports = function(app) { app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); ! routes.forEach(function(route) { route(app); }); }; We have a mock server!
  58. Concert HTTP Mock module.exports = function(app) { var express =

    require('express'); var concertRouter = express.Router(); concertRouter.get('/', function(req, res) { res.send({concerts:[]}); }); app.use('/concerts', concertRouter); };
  59. Concert HTTP Mock module.exports = function(app) { var express =

    require('express'); var concertRouter = express.Router(); concertRouter.get('/', function(req, res) { res.send({concerts:[]}); }); app.use('/concerts', concertRouter); };
  60. concertRouter.get(‘/:id', function(req, res) { res.send({concert: CONCERTS[req.params.id]}); }); concertRouter.post('/', function(req, res)

    { res.status(201).send({ concert: fakeConcert() }); }); concertRouter.post('/', function(req, res) { res.status(422).send({ errors: { concert: [“Those bands are lame.”] } }); });
  61. concertRouter.get(‘/:id', function(req, res) { res.send({concert: CONCERTS[req.params.id]}); }); concertRouter.post('/', function(req, res)

    { res.status(201).send({ concert: fakeConcert() }); }); concertRouter.post('/', function(req, res) { res.status(422).send({ errors: { concert: [“Those bands are lame.”] } }); });
  62. concertRouter.get(‘/:id', function(req, res) { res.send({concert: CONCERTS[req.params.id]}); }); concertRouter.post('/', function(req, res)

    { res.status(201).send({ concert: fakeConcert() }); }); concertRouter.post('/', function(req, res) { res.status(422).send({ errors: { concert: [“Those bands are lame.”] } }); });
  63. Hardcoded values If your mock is short-lived, this may be

    sufficient.
  64. … var CONCERTS = [ { id: 1, date: new

    Date(‘2014-06-22’), band_ids: [1,2,3], venue_id: 999 }, { id: 2, date: new Date(‘2014-03-11’), band_ids: [1], venue_id: 999 } ]; ! var BANDS = [ { id: 1, name: “Bad Motha Goose” }, { id: 2, name: “Concrete Octopus” }, { id: 3, name: “Full Metal Chicken” }, ]; ! var VENUES = [ { id: 999, name: “Stubb’s”} ]; ! concertRouter.get('/', function(req, res) { res.send({ concerts: CONCERTS, bands: BANDS, venues: VENUES }); }); …
  65. … var CONCERTS = [ { id: 1, date: new

    Date(‘2014-06-22’), band_ids: [1,2,3], venue_id: 999 }, { id: 2, date: new Date(‘2014-03-11’), band_ids: [1], venue_id: 999 } ]; …
  66. … var BANDS = [ { id: 1, name: “Bad

    Motha Goose” }, { id: 2, name: “Concrete Octopus” }, { id: 3, name: “Full Metal Chicken” }, ]; …
  67. … var VENUES = [ { id: 999, name: “Stubb’s”}

    ]; …
  68. … concertRouter.get('/', function(req, res) { res.send({ concerts: CONCERTS, bands: BANDS,

    venues: VENUES }); }); …
  69. Make it better functions moment.js faker.js

  70. require(‘faker’); ! var fakeConcert = function() { { id: faker.Helpers.randomNumber(999),

    name: faker.Company.companyName() date: faker.Date.future(1) } };
  71. require(‘moment’); ! var fakeConcert = function() { { id: 123,

    name: “House of Blues”, date: moment.add(15, ‘days’) } };
  72. Caveats Mocks directory is not watched. Don’t forget to restart!

  73. Write code ☼

  74. None
  75. Let Ember drive the API If you’re implementing both front-end

    and back-end, wait to do the back-end code. ☼
  76. Let Ember drive the API - sideloading - metadata (pagination)

  77. Consult the mocks HTTP Mocks become a reference / spec

    for implementing your API.
  78. When API is ready: Trash steps 1 and 2! ☼

  79. > git rm app/adapters/concert.js > git rm app/mocks/concert.js

  80. ☼ Developing a feature • Create an adapter • Add

    HTTP Mocks • Code • Let Ember drive the API • When API is ready, trash steps 1 & 2
  81. Can’t use ember-cli? Make your own.

  82. A tiny express app package.json server.js ☼

  83. { "name": "mock-server", "description": “I’m scared of ember-cli“, "version": "0.0.1",

    "private": true, "dependencies": { "express": "4.1.x", "cors": "2.2.0" } } package.json
  84. var express = require('express'), cors = require('cors'), app = express();

    app.use(cors()); app.get('/concerts/:id', function(req, res){ res.json({concerts: CONCERTS[req.params.id]}); }); var server = app.listen(6005, function() { console.log('Listening on port %d', server.address().port); }); server.js
  85. > npm install > npm start

  86. Ember Data alternative You can use pushPayload. ☼

  87. App.ApplicationSerializer = DS.ActiveModelSerializer; ! App.Post = DS.Model.extend… App.Comment = DS.Model.extend…

    ! App.IndexRoute = Ember.Route.extend({ model: function() { var pushData = { posts: [ {id: 1, post_title: "Great post", comment_ids: [2]} ], comments: [ {id: 2, comment_body: "first"} ] }; ! this.store.pushPayload(pushData); return this.store.find('post', 1); } });
  88. App.ApplicationSerializer = DS.ActiveModelSerializer; ! App.Post = DS.Model.extend… App.Comment = DS.Model.extend…

    ! App.IndexRoute = Ember.Route.extend({ model: function() { var pushData = { posts: [ {id: 1, post_title: "Great post", comment_ids: [2]} ], comments: [ {id: 2, comment_body: "first"} ] }; ! this.store.pushPayload(pushData); return this.store.find('post', 1); } });
  89. App.ApplicationSerializer = DS.ActiveModelSerializer; ! App.Post = DS.Model.extend… App.Comment = DS.Model.extend…

    ! App.IndexRoute = Ember.Route.extend({ model: function() { var pushData = { posts: [ {id: 1, post_title: "Great post", comment_ids: [2]} ], comments: [ {id: 2, comment_body: "first"} ] }; ! this.store.pushPayload(pushData); return this.store.find('post', 1); } });
  90. App.ApplicationSerializer = DS.ActiveModelSerializer; ! App.Post = DS.Model.extend… App.Comment = DS.Model.extend…

    ! App.IndexRoute = Ember.Route.extend({ model: function() { var pushData = { posts: [ {id: 1, post_title: "Great post", comment_ids: [2]} ], comments: [ {id: 2, comment_body: "first"} ] }; ! this.store.pushPayload(pushData); return this.store.find('post', 1); } });
  91. App.ApplicationSerializer = DS.ActiveModelSerializer; ! App.Post = DS.Model.extend… App.Comment = DS.Model.extend…

    ! App.IndexRoute = Ember.Route.extend({ model: function() { var pushData = { posts: [ {id: 1, post_title: "Great post", comment_ids: [2]} ], comments: [ {id: 2, comment_body: "first"} ] }; ! this.store.pushPayload(pushData); return this.store.find('post', 1); } });
  92. Advantage Quick way to try out something

  93. Drawback Must be done this way whenever you load records.

  94. Drawback Adds cruft to routes

  95. Make your own Use a tiny express app - run

    it as another server ! Ember Data Alternative - pushPayload ☼ ☼
  96. What we talked about: Mock Server? HTTP Mock? Who can

    benefit Why mocks are better than fixtures Walkthrough developing a feature How to make your own ☼ ☼ ☼ ☼ ☼ ☼
  97. Thank You! Chris Ball @cball_