SlideShare a Scribd company logo
Rapid prototyping and
easy testing with Ember
CLI Mirage
1
Me
• Name: Krzysztof Białek
• Github:
github.com/krzysztofbialek
• Twitter: drwrum
• Company: Rebased
SPA and dynamic server
data
• No backend api present yet
• Backend api not yet stable
• Mocking api in acceptance testing
Dealing with it - testing
with sinon
let project = JSON.stringify({
project: {
id: 1000,
links: {
devices: '/web_api/projects/1000/devices'
}
}
});
server.respondWith('GET', '/web_api/projects/1000', [200,
{"Content-Type":"application/json"}, project]);
Dealing with it - testing
with sinon
/* global server */
import Ember from 'ember';
export default Ember.Test.registerHelper('respondGET', function(app,
url, payload) {
let data = JSON.stringify(payload);
server.respondWith('GET', url, [200, {"Content-
Type":"application/json"}, data]);
});
respondGET('/web_api/projects/1000/devices', {
devices: [
{id: 1, name: "ipad"}
]
});
Dealing with it - Mocking with
ember cli http mocks
//server/mocks/author.js
module.exports = function(app) {
var express = require('express');
var authorRouter = express.Router();
var AUTHORS = [
{
id: 1,
name: 'Bad Motha Goose'
},
{
id: 2,
name: 'Concrete Octopus'
}
];
authorRouter.get('/', function(req, res) {
var ids = req.query.ids;
var authors;
if (ids) {
authors = authors.filter(function(b) {
return ids.indexOf(b.id.toString()) > -1;
});
}
else {
authors = authors;
}
res.send({"authors": authors});
});
app.use('/authors', authorRouter);
};
Dealing with it - the right
way
What can Ember CLI
Mirage do for you?
• Mocking of api in development
• Mocking endpoints in testing
• With it’s own ORM it can mimic ember-data
behaviour
• Bundled into application
• wraps around pretender.js
Define simple route
// mirage/config.js
export default function() {
this.namespace = 'api';
this.get('/authors', () => {
return {
authors: [
{id: 1, name: 'Zelda'},
{id: 2, name: 'Link'},
{id: 3, name: 'Epona'},
]
};
});
};
Define simple route
this.get('/events', () => {
let events = [];
for (let i = 1; i < 1000; i++) {
events.push({id: i, value: Math.random()});
};
return events;
});
this.get('/users/current', () => {
return {
name: 'Zelda',
email: 'z@hyrule.org'
};
});
Why so verbose?
// mirage/config.js
export default function() {
this.get('/authors', () => {
return {
authors: [
{id: 1, name: 'Zelda'},
{id: 2, name: 'Link'},
{id: 3, name: 'Epona'},
]
};
});
};
// mirage/config.js
export default function() {
this.get('/authors', (schema) => {
return schema.authors.all;
});
};
Data layer
• In memory database
• Models
• Factories
• Serializers
Going flexible with
models
// mirage/models/author.js
import { Model } from 'ember-cli-mirage';
export default Model;
// and use it in the route
// mirage/config.js
export default function() {
this.get('/authors', (schema) => {
return schema.authros.all;
});
}
Going flexible with
models
// app/routes/some-route
Ember.createObject('author', { name: 'Link', age: 123 })
// payload send with the request to ‘authors’ route
author: {
name: 'Link',
age: 123
}
// mirage/config.js
this.post('/authors', (schema, request) => {
let attrs = JSON.parse(request.requestBody).author;
return schema.authors.create(attrs);
});
Model associations
// mirage/models/author.js
import { Model, hasMany } from 'ember-cli-mirage';
export default Model.extend({
posts: hasMany()
});
// mirage/config.js
this.del('/authors/:id', (schema, request) => {
let author = schema.authors.find(request.params.id);
author.posts.delete();
author.delete();
});
Dynamic routes and
query params
this.get('/authors/:id', (schema, request) => {
let id = request.params.id;
return schema.authors.find(id);
});
this.get('/authors', (schema, request) => {
let someQuery = request.queryParams[‘some_query’];
return schema.authors.where({name: someQuery});
});
What if API is weird?
// mirage/serializers/application.js
import { Serializer } from 'ember-cli-mirage';
import Ember from 'ember';
const { dasherize } = Ember.String;
export default Serializer.extend({
keyForAttribute(key) {
return dasherize(key);
}
});
GET /authors/1
{
author: {
id: 1,
'first-name': 'Keyser',
'last-name': 'Soze',
age: 145
}
}
Taming API with
serialiser
Lets fake some data!
• Fixtures
• Factories
Fixtures - quick and rigid
// /mirage/fixtures/authors.js
export default [
{id: 1, firstName: 'Link'},
{id: 2, firstName: 'Zelda'}
];
// /mirage/fixtures/blog-posts.js
export default [
{id: 1, title: 'Lorem', authorId: 1},
{id: 2, title: 'Ipsum', authorId: 1},
{id: 3, title: 'Dolor', authorId: 2}
];
// mirage/scenarios/default.js
export default function(server) {
server.loadFixtures();
};
But…
Factories - also quick
but flexible
// mirage/factories/author.js
import { Factory } from 'ember-cli-mirage';
export default Factory.extend({
name(i) {
return `Author ${i}`;
},
age: 20,
admin: false
});
server.createList('author', 3);
{id: 1, name: "Author 1", age: 20, admin: false}
{id: 2, name: "Author 2", age: 20, admin: false}
{id: 3, name: "Author 3", age: 20, admin: false}
Factories - making data up
since… not so long ago
// mirage/factories/author.js
import { Factory, faker } from 'ember-cli-mirage';
export default Factory.extend({
firstName() {
return faker.name.firstName();
},
lastName() {
return faker.name.lastName();
},
age() {
// list method added by Mirage
return faker.list.random(18, 20, 28, 32, 45, 60)();
},
});
Factories - seeding data
in development
// mirage/scenarios/default.js
export default function(server) {
server.createList('blog-post', 10);
let author = server.create('author', {name: 'Zelda'});
server.createList('blog-post', 20, { author });
};
Factories - seeding data
in tests
test('I can view the photos', assert => {
server.createList('photo', 10);
visit('/');
andThen(function() {
assert.equal( find('img').length, 10 );
});
});
Acceptance testing -
overriding defaults
// mirage/factories/photo.js
import Mirage from 'ember-cli-mirage';
export default Mirage.Factory.extend({
title(i) {
// Photo 1, Photo 2 etc.
return `Photo ${i}`;
}
});
test("I can view the photos", assert => {
server.createList('photo', 10);
visit('/');
andThen(() => {
assert.equal( find('img').length, 10 );
});
});
Acceptance testing -
overriding defaults
test("I see the photo's title on a detail route", assert => {
let photo = server.create('photo', {title: 'Sunset over
Hyrule'});
visit('/' + photo.id);
andThen(() => {
assert.equal( find('h1:contains(Sunset over
Hyrule)').length, 1 );
});
});
Acceptance testing - make
sure server was called
test("I can change the lesson's title", assert => {
server.create('lesson', {title: 'My First Lesson'})
visit('/');
click('.Edit')
fillIn('input', 'Updated lesson');
click('.Save');
andThen(() => {
// Assert against our app's UI
assert.equal( find('h1:contains(Updated lesson)').length, 1
);
// Also check that the data was "persisted" to our backend
assert.equal( server.db.lessons[0].title, 'Updated
lesson');
});
});
Acceptance testing -
testing errors
test('the user sees an error if the save attempt fails',
function(assert) {
server.post('/questions', {errors: ['There was an error']},
500);
visit('/');
click('.new');
fillIn('input', 'New question');
click('.save');
andThen(() => {
assert.equals(find('p:contains(There was an
error)').length, 1);
});
});
That could be enough to
start…
Rapid prototyping and easy testing with ember cli mirage
Shorthands
// Expanded
this.get('/contacts', ({ contacts }) => {
return contacts.all(); // users in the second case
});
// Shorthand
this.get('/contacts'); // finds type by singularizing url
this.get('/contacts', 'users'); // optionally specify the collection as
second param
// Expanded
this.del('/contacts/:id', ({ contacts }, request) => {
let id = request.params.id;
let contact = contacts.find(id);
contact.addresses.destroy();
contact.destroy();
});
// Shorthand
this.del('/contacts/:id', ['contact', 'addresses']);
Easy peasy crud
// Resource
this.resource('contacts'); // available in 0.2.2+
// Equivalent shorthands
this.get('/contacts');
this.get('/contacts/:id');
this.post('/contacts');
this.put('/contacts/:id');
this.patch('/contacts/:id');
this.del('/contacts/:id');
Mix real api with fake
endpoints
this.passthrough('/addresses', '/contacts');
this.passthrough('/something');
this.passthrough('/else');
// just some verbs
this.passthrough('/addresses', ['post']);
this.passthrough('/contacts', '/photos', ['get']);
// other-origin
this.passthrough('http://api.foo.bar/**');
this.passthrough('http://api.twitter.com/v1/cards/**');
Happy faking!!!
• http://www.ember-cli-mirage.com
• https://github.com/pretenderjs/pretender

More Related Content

Rapid prototyping and easy testing with ember cli mirage

  • 1. Rapid prototyping and easy testing with Ember CLI Mirage 1
  • 2. Me • Name: Krzysztof Białek • Github: github.com/krzysztofbialek • Twitter: drwrum • Company: Rebased
  • 3. SPA and dynamic server data • No backend api present yet • Backend api not yet stable • Mocking api in acceptance testing
  • 4. Dealing with it - testing with sinon let project = JSON.stringify({ project: { id: 1000, links: { devices: '/web_api/projects/1000/devices' } } }); server.respondWith('GET', '/web_api/projects/1000', [200, {"Content-Type":"application/json"}, project]);
  • 5. Dealing with it - testing with sinon /* global server */ import Ember from 'ember'; export default Ember.Test.registerHelper('respondGET', function(app, url, payload) { let data = JSON.stringify(payload); server.respondWith('GET', url, [200, {"Content- Type":"application/json"}, data]); }); respondGET('/web_api/projects/1000/devices', { devices: [ {id: 1, name: "ipad"} ] });
  • 6. Dealing with it - Mocking with ember cli http mocks //server/mocks/author.js module.exports = function(app) { var express = require('express'); var authorRouter = express.Router(); var AUTHORS = [ { id: 1, name: 'Bad Motha Goose' }, { id: 2, name: 'Concrete Octopus' } ]; authorRouter.get('/', function(req, res) { var ids = req.query.ids; var authors; if (ids) { authors = authors.filter(function(b) { return ids.indexOf(b.id.toString()) > -1; }); } else { authors = authors; } res.send({"authors": authors}); }); app.use('/authors', authorRouter); };
  • 7. Dealing with it - the right way
  • 8. What can Ember CLI Mirage do for you? • Mocking of api in development • Mocking endpoints in testing • With it’s own ORM it can mimic ember-data behaviour • Bundled into application • wraps around pretender.js
  • 9. Define simple route // mirage/config.js export default function() { this.namespace = 'api'; this.get('/authors', () => { return { authors: [ {id: 1, name: 'Zelda'}, {id: 2, name: 'Link'}, {id: 3, name: 'Epona'}, ] }; }); };
  • 10. Define simple route this.get('/events', () => { let events = []; for (let i = 1; i < 1000; i++) { events.push({id: i, value: Math.random()}); }; return events; }); this.get('/users/current', () => { return { name: 'Zelda', email: 'z@hyrule.org' }; });
  • 11. Why so verbose? // mirage/config.js export default function() { this.get('/authors', () => { return { authors: [ {id: 1, name: 'Zelda'}, {id: 2, name: 'Link'}, {id: 3, name: 'Epona'}, ] }; }); }; // mirage/config.js export default function() { this.get('/authors', (schema) => { return schema.authors.all; }); };
  • 12. Data layer • In memory database • Models • Factories • Serializers
  • 13. Going flexible with models // mirage/models/author.js import { Model } from 'ember-cli-mirage'; export default Model; // and use it in the route // mirage/config.js export default function() { this.get('/authors', (schema) => { return schema.authros.all; }); }
  • 14. Going flexible with models // app/routes/some-route Ember.createObject('author', { name: 'Link', age: 123 }) // payload send with the request to ‘authors’ route author: { name: 'Link', age: 123 } // mirage/config.js this.post('/authors', (schema, request) => { let attrs = JSON.parse(request.requestBody).author; return schema.authors.create(attrs); });
  • 15. Model associations // mirage/models/author.js import { Model, hasMany } from 'ember-cli-mirage'; export default Model.extend({ posts: hasMany() }); // mirage/config.js this.del('/authors/:id', (schema, request) => { let author = schema.authors.find(request.params.id); author.posts.delete(); author.delete(); });
  • 16. Dynamic routes and query params this.get('/authors/:id', (schema, request) => { let id = request.params.id; return schema.authors.find(id); }); this.get('/authors', (schema, request) => { let someQuery = request.queryParams[‘some_query’]; return schema.authors.where({name: someQuery}); });
  • 17. What if API is weird?
  • 18. // mirage/serializers/application.js import { Serializer } from 'ember-cli-mirage'; import Ember from 'ember'; const { dasherize } = Ember.String; export default Serializer.extend({ keyForAttribute(key) { return dasherize(key); } }); GET /authors/1 { author: { id: 1, 'first-name': 'Keyser', 'last-name': 'Soze', age: 145 } } Taming API with serialiser
  • 19. Lets fake some data! • Fixtures • Factories
  • 20. Fixtures - quick and rigid // /mirage/fixtures/authors.js export default [ {id: 1, firstName: 'Link'}, {id: 2, firstName: 'Zelda'} ]; // /mirage/fixtures/blog-posts.js export default [ {id: 1, title: 'Lorem', authorId: 1}, {id: 2, title: 'Ipsum', authorId: 1}, {id: 3, title: 'Dolor', authorId: 2} ]; // mirage/scenarios/default.js export default function(server) { server.loadFixtures(); };
  • 22. Factories - also quick but flexible // mirage/factories/author.js import { Factory } from 'ember-cli-mirage'; export default Factory.extend({ name(i) { return `Author ${i}`; }, age: 20, admin: false }); server.createList('author', 3); {id: 1, name: "Author 1", age: 20, admin: false} {id: 2, name: "Author 2", age: 20, admin: false} {id: 3, name: "Author 3", age: 20, admin: false}
  • 23. Factories - making data up since… not so long ago // mirage/factories/author.js import { Factory, faker } from 'ember-cli-mirage'; export default Factory.extend({ firstName() { return faker.name.firstName(); }, lastName() { return faker.name.lastName(); }, age() { // list method added by Mirage return faker.list.random(18, 20, 28, 32, 45, 60)(); }, });
  • 24. Factories - seeding data in development // mirage/scenarios/default.js export default function(server) { server.createList('blog-post', 10); let author = server.create('author', {name: 'Zelda'}); server.createList('blog-post', 20, { author }); };
  • 25. Factories - seeding data in tests test('I can view the photos', assert => { server.createList('photo', 10); visit('/'); andThen(function() { assert.equal( find('img').length, 10 ); }); });
  • 26. Acceptance testing - overriding defaults // mirage/factories/photo.js import Mirage from 'ember-cli-mirage'; export default Mirage.Factory.extend({ title(i) { // Photo 1, Photo 2 etc. return `Photo ${i}`; } }); test("I can view the photos", assert => { server.createList('photo', 10); visit('/'); andThen(() => { assert.equal( find('img').length, 10 ); }); });
  • 27. Acceptance testing - overriding defaults test("I see the photo's title on a detail route", assert => { let photo = server.create('photo', {title: 'Sunset over Hyrule'}); visit('/' + photo.id); andThen(() => { assert.equal( find('h1:contains(Sunset over Hyrule)').length, 1 ); }); });
  • 28. Acceptance testing - make sure server was called test("I can change the lesson's title", assert => { server.create('lesson', {title: 'My First Lesson'}) visit('/'); click('.Edit') fillIn('input', 'Updated lesson'); click('.Save'); andThen(() => { // Assert against our app's UI assert.equal( find('h1:contains(Updated lesson)').length, 1 ); // Also check that the data was "persisted" to our backend assert.equal( server.db.lessons[0].title, 'Updated lesson'); }); });
  • 29. Acceptance testing - testing errors test('the user sees an error if the save attempt fails', function(assert) { server.post('/questions', {errors: ['There was an error']}, 500); visit('/'); click('.new'); fillIn('input', 'New question'); click('.save'); andThen(() => { assert.equals(find('p:contains(There was an error)').length, 1); }); });
  • 30. That could be enough to start…
  • 32. Shorthands // Expanded this.get('/contacts', ({ contacts }) => { return contacts.all(); // users in the second case }); // Shorthand this.get('/contacts'); // finds type by singularizing url this.get('/contacts', 'users'); // optionally specify the collection as second param // Expanded this.del('/contacts/:id', ({ contacts }, request) => { let id = request.params.id; let contact = contacts.find(id); contact.addresses.destroy(); contact.destroy(); }); // Shorthand this.del('/contacts/:id', ['contact', 'addresses']);
  • 33. Easy peasy crud // Resource this.resource('contacts'); // available in 0.2.2+ // Equivalent shorthands this.get('/contacts'); this.get('/contacts/:id'); this.post('/contacts'); this.put('/contacts/:id'); this.patch('/contacts/:id'); this.del('/contacts/:id');
  • 34. Mix real api with fake endpoints this.passthrough('/addresses', '/contacts'); this.passthrough('/something'); this.passthrough('/else'); // just some verbs this.passthrough('/addresses', ['post']); this.passthrough('/contacts', '/photos', ['get']); // other-origin this.passthrough('http://api.foo.bar/**'); this.passthrough('http://api.twitter.com/v1/cards/**');
  • 35. Happy faking!!! • http://www.ember-cli-mirage.com • https://github.com/pretenderjs/pretender

Editor's Notes

  1. - using sinon pretender.js can be used as well define data and then server mock
  2. or you can use a wrapper to make it easier need to define it each time
  3. starts express js server needs node :( only mocking, no testing
  4. no need to have separate server can be used in production environment pretender can be configured with any options from it’s doc
  5. namespace like in adapters all calls to this url will be responded by mirage all routes in the config file namespace only for routes defined later handlers use common verbs
  6. namespace like in adapters all calls to this url will be responded by mirage all routes in the config file namespace only for routes defined later handlers use common verbs
  7. nicer for endpoints when we want to get all that is in database or some particular data as we will see in a bit
  8. sets database at runtime available generators schema is the instance of database to freely interact with
  9. model persisted in the database id assigned so ember-data can do it’s magic data is persisted in the single session to simulate real life exp same goes for other verbs, deleting models, retrieving collections searching
  10. finder method response codes
  11. like different key format that ember may expect with adapter app uses When you return a model or a collection from a route handler, Mirage serializes it into a JSON payload, and then responds to your Ember app with that payload. mirage ships with serialisers for jsonapi, rest and active model adapters it’s important to make the payload in same way like server will
  12. by default it takes all the attributes of the model and wraps it under model name key let’s assume server sends dasherized keys
  13. ok but we still need to get these responses payload from somewhere, right? couple of ways to do it
  14. think of them as database table loaded into mirage db return arrays of POJOS scenario js file to define what data should be loaded into db
  15. - it’s not the mirage way and it gets inconvenient very quickly
  16. - access to sequence which is only parameter available in factory
  17. faker there are more for example avatar or other images
  18. access to server create and create lists
  19. mirage is bundles with ember app in testing db is populated when app is booting reset after each test
  20. when testing UI we test to see changes but not always that means actually call to server was made. can be also tested by overriding route and testing against request sent by ember
  21. only valid route for the duration of test if it overrides something from config file it’s just for the test
  22. - mirage has a lot more to offer so I’ll highlight few other sweet features
  23. - we don’t want to write code we don’t need to
  24. - can be whitelisted or constrained to certain verbs