rapid prototyping and easy testing with ember cli mirage
TRANSCRIPT
Rapid prototyping and easy testing with
Ember CLI Mirage
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.jsmodule.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.jsexport default function() { this.namespace = 'api'; this.get('/authors', () => { return { authors: [ {id: 1, name: 'Zelda'}, {id: 2, name: 'Link'}, {id: 3, name: 'Epona'}, ] }; });};
Define simple routethis.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: '[email protected]' };});
Why so verbose?// mirage/config.jsexport default function() {
this.get('/authors', () => { return { authors: [ {id: 1, name: 'Zelda'}, {id: 2, name: 'Link'}, {id: 3, name: 'Epona'}, ] }; });};
// mirage/config.jsexport 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.jsimport { Model } from 'ember-cli-mirage';
export default Model;
// and use it in the route
// mirage/config.jsexport default function() { this.get('/authors', (schema) => { return schema.authros.all; });}
Going flexible with models
// app/routes/some-routeEmber.createObject('author', { name: 'Link', age: 123 })
// payload send with the request to ‘authors’ route author: { name: 'Link', age: 123 }
// mirage/config.jsthis.post('/authors', (schema, request) => { let attrs = JSON.parse(request.requestBody).author;
return schema.authors.create(attrs);});
Model associations// mirage/models/author.jsimport { Model, hasMany } from 'ember-cli-mirage';
export default Model.extend({ posts: hasMany()});
// mirage/config.jsthis.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.jsimport { 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.jsexport default [ {id: 1, firstName: 'Link'}, {id: 2, firstName: 'Zelda'}];
// /mirage/fixtures/blog-posts.jsexport default [ {id: 1, title: 'Lorem', authorId: 1}, {id: 2, title: 'Ipsum', authorId: 1}, {id: 3, title: 'Dolor', authorId: 2}];
// mirage/scenarios/default.jsexport default function(server) { server.loadFixtures();};
But…
Factories - also quick but flexible
// mirage/factories/author.jsimport { 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.jsimport { 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.jsexport 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.jsimport 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
calledtest("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…
Shorthands // Expanded this.get('/contacts', ({ contacts }) => { return contacts.all(); // users in the second case});
// Shorthand this.get('/contacts'); // finds type by singularizing urlthis.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 verbsthis.passthrough('/addresses', ['post']);this.passthrough('/contacts', '/photos', ['get']);
// other-originthis.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