$30 off During Our Annual Pro Sale. View Details »

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.

Chris Ball

August 14, 2014
Tweet

More Decks by Chris Ball

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  5. Chris Ball
    @cball_

    View Slide

  6. thanks to this guy

    View Slide

  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






    View Slide

  8. Mock Server?
    HTTP Mock?

    View Slide

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

    View Slide

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

    View Slide

  11. Mock Server
    Often used for testing external services
    (tests still work on a plane!)

    View Slide

  12. Mock Server
    ember-cli has this built in as of
    v0.0.28

    View Slide

  13. HTTP Mock
    Fake API response used by
    mock server

    View Slide

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

    HTTP Mock

    View Slide

  15. Tell a mock server to respond
    with an HTTP mock and you
    have a fake API
    Mock Server + HTTP Mock

    View Slide

  16. sinon.js
    pretender

    Mock Server + HTTP Mock

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  20. Who can benefit from
    using HTTP Mocks?

    View Slide

  21. Solo Developer
    Avoid context switching
    between apps.

    View Slide

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

    View Slide

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

    View Slide

  24. ☼ Who can benefit?
    • Solo Developer
    • Front-end Team
    • Ember Data Learner
    • Everyone!

    View Slide

  25. Why HTTP Mocks are
    better than fixtures.

    View Slide

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

    View Slide

  27. Keep the same adapters
    HTTP Mocks will allow you to
    use the same adapters as the
    rest of your app.

    View Slide

  28. RESTAdapter
    Expects api will return json in
    camelCase.

    View Slide

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

    View Slide

  30. ActiveModelAdapter
    Expects api will return json in
    snake_case.

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  34. Why this helps
    “Use this as a mock response.
    The final API will look almost
    identical.”

    View Slide

  35. {
    "person": {
    "first_name": "Devin",
    "last_name": "Townsend",
    "occupation": “Musician”,
    “image_ids”: [1]
    },
    "images": {
    "id": 1
    "url": "http://myimage.gif"
    }
    }

    View Slide

  36. If using a fixture
    Here’s what your model would
    look like:

    View Slide

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

    View Slide

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

    View Slide

  39. Fixtures root json
    Our API response also has an
    outer “person” wrapper.

    View Slide

  40. Predict app behavior
    Especially important for non-
    standard API’s
    (sideloading, serializers)

    View Slide

  41. Make ajax requests
    Example: build out behavior for
    an ajax error event

    View Slide

  42. Fixtures always resolve
    screenshot of code

    View Slide

  43. Drive the API spec
    A lot of hard API decisions can
    be made for you

    View Slide

  44. jsonapi.org
    Great reference for API design.

    View Slide

  45. Steps from mock live
    (almost always)

    View Slide

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

    View Slide

  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

    View Slide

  48. Let’s walk through
    development of a new
    feature.

    View Slide

  49. Background
    We’re building a concert
    ticketing system

    View Slide

  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’)
    });

    View Slide

  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)

    View Slide

  52. Build out Concert
    export default DS.Model.extend({
    date: DS.attr(‘date’),
    bands: DS.hasMany(‘band’),
    venue: DS.belongsTo(‘venue’)
    });

    View Slide

  53. Create an adapter

    Set the host to ember app

    View Slide

  54. # app/adapters/concert.js
    import DS from ‘ember-data’;
    !
    export default DS.ActiveModelAdapter.extend({
    host: ‘http://localhost:4200'
    });

    View Slide

  55. Add HTTP Mock

    View Slide

  56. > ember g http-mock concert

    View Slide

  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!

    View Slide

  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);
    };

    View Slide

  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);
    };

    View Slide

  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.”]
    }
    });
    });

    View Slide

  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.”]
    }
    });
    });

    View Slide

  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.”]
    }
    });
    });

    View Slide

  63. Hardcoded values
    If your mock is short-lived, this
    may be sufficient.

    View Slide


  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
    });
    });

    View Slide


  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
    }
    ];

    View Slide


  66. var BANDS = [
    { id: 1, name: “Bad Motha Goose” },
    { id: 2, name: “Concrete Octopus” },
    { id: 3, name: “Full Metal Chicken” },
    ];

    View Slide


  67. var VENUES = [
    { id: 999, name: “Stubb’s”}
    ];

    View Slide


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

    View Slide

  69. Make it better
    functions
    moment.js
    faker.js

    View Slide

  70. require(‘faker’);
    !
    var fakeConcert = function() {
    {
    id: faker.Helpers.randomNumber(999),
    name: faker.Company.companyName()
    date: faker.Date.future(1)
    }
    };

    View Slide

  71. require(‘moment’);
    !
    var fakeConcert = function() {
    {
    id: 123,
    name: “House of Blues”,
    date: moment.add(15, ‘days’)
    }
    };

    View Slide

  72. Caveats
    Mocks directory is not watched.
    Don’t forget to restart!

    View Slide

  73. Write code

    View Slide

  74. View Slide

  75. Let Ember drive the API
    If you’re implementing both
    front-end and back-end, wait to
    do the back-end code.

    View Slide

  76. Let Ember drive the API
    - sideloading
    - metadata (pagination)

    View Slide

  77. Consult the mocks
    HTTP Mocks become a
    reference / spec for
    implementing your API.

    View Slide

  78. When API is ready:
    Trash steps 1 and 2!

    View Slide

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

    View Slide

  80. ☼ Developing a feature
    • Create an adapter
    • Add HTTP Mocks
    • Code
    • Let Ember drive the API
    • When API is ready, trash steps 1 & 2

    View Slide

  81. Can’t use ember-cli?
    Make your own.

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  85. > npm install
    > npm start

    View Slide

  86. Ember Data alternative
    You can use pushPayload.

    View Slide

  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);
    }
    });

    View Slide

  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);
    }
    });

    View Slide

  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);
    }
    });

    View Slide

  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);
    }
    });

    View Slide

  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);
    }
    });

    View Slide

  92. Advantage
    Quick way to try out something

    View Slide

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

    View Slide

  94. Drawback
    Adds cruft to routes

    View Slide

  95. Make your own
    Use a tiny express app
    - run it as another server
    !
    Ember Data Alternative
    - pushPayload


    View Slide

  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






    View Slide

  97. Thank You!
    Chris Ball
    @cball_

    View Slide