Feel the thrill and enjoyment of testing when using Factories instead of Fixtures. Factories simplify the process of testing, making you more efficient and your tests more readable.
Questions: Slack => factory-guy
Contents:
- Installation
- How This Works
- Setup
- Defining Factories
- Using Factories
- Custom API formats
- Sequences
- Inline Function
- Traits
- Associations
- Extending Other Definitions
- Callbacks
- Testing - creating scenarios
- Testing models, controllers, components
- Acceptance Tests
ChangeLog: ( Notes about what has changed in each version )
ember install ember-data-factory-guy@2.4.2
( ember-data-1.13.5+ )ember install ember-data-factory-guy@1.13.2
( ember-data-1.13.0 + )ember install ember-data-factory-guy@1.1.2
( ember-data-1.0.0-beta.19.1 )ember install ember-data-factory-guy@1.0.10
( ember-data-1.0.0-beta.16.1 )
- remove ember-data-factory-guy from package.json
npm prune
ember install ember-data-factory-guy ( for latest release )
- You create factories for you models.
- put them in tests/factories directory
- Then you use them to create models in your tests.
- You can make records that persist in the store
- Or you can build a json payload used for mocking an ajax call's payload
In the following examples, assume the models look like this:
// standard models
User = DS.Model.extend({
name: DS.attr('string'),
style: DS.attr('string'),
projects: DS.hasMany('project'),
hats: DS.hasMany('hat', {polymorphic: true})
});
Project = DS.Model.extend({
title: DS.attr('string'),
user: DS.belongsTo('user')
});
// polymorphic models
Hat = DS.Model.extend({
type: DS.attr('string'),
user: DS.belongsTo('user')
});
BigHat = Hat.extend();
SmallHat = Hat.extend();
- A factory has a name and a set of attributes.
- The name should match the model type name. So, for 'User' model, the name would be 'user'
- Create factory files in the tests/factories directory.
- Can use generator to create the outline of a factory file:
ember g factory user
This will create a file named user.js in the tests/factories directory.
// file tests/factories/user.js
import FactoryGuy from 'ember-data-factory-guy';
FactoryGuy.define('user', {
// Put default 'user' attributes in the default section
default: {
style: 'normal',
name: 'Dude'
},
// Create a named 'user' with custom attributes
admin: {
style: 'super',
name: 'Admin'
}
});
- Define each polymorphic model in it's own typed definition
- May want to extend parent factory here
// file tests/factories/small-hat.js
import FactoryGuy from 'ember-data-factory-guy';
FactoryGuy.define('small-hat', {
default: {
type: 'SmallHat'
}
})
// file tests/factories/big-hat.js
import FactoryGuy from 'ember-data-factory-guy';
FactoryGuy.define('big-hat', {
default: {
type: 'BigHat'
}
})
In other words, don't do this:
// file tests/factories/hat.js
import FactoryGuy from 'ember-data-factory-guy';
FactoryGuy.define('hat', {
default: {},
small-hat: {
type: 'SmallHat'
},
big-hat: {
type: 'BigHat'
}
})
- FactoryGuy.make
- Loads model instance into the store
- FactoryGuy.makeList
- Loads zero to many model instances into the store
- FactoryGuy.build
- Builds json in accordance with the adapters specifications
- RESTAdapter (assume this adapter being used in most of the following examples)
- ActiveModelAdapter
- JSONAPIAdapter
- Builds json in accordance with the adapters specifications
- FactoryGuy.buildList
- Builds json with list of zero or more items in accordance with the adapters specifications
- Can override default attributes by passing in an object of options
- Can add attributes or relationships with traits
- Can compose relationships
- By passing in other objects you've made with build/buildList or make/makeList
- all instances loaded into the ember data store
Usage:
import { make } from 'ember-data-factory-guy';
// make basic user with the default attributes in user factory
let user = make('user');
user.toJSON({includeId: true}) // => {id: 1, name: 'User1', style: 'normal'}
// make user with default attributes plus those defined as 'admin' in user factory
let user = make('admin');
user.toJSON({includeId: true}) // => {id: 2, name: 'Admin', style: 'super'}
// make user with default attributes plus these extra attributes
let user = make('user', {name: 'Fred'});
user.toJSON({includeId: true}) // => {id: 3, name: 'Fred', style: 'normal'}
// make admin defined user with these extra attributes
let user = make('admin', {name: 'Fred'});
user.toJSON({includeId: true}) // => {id: 4, name: 'Fred', style: 'super'}
// make default user with traits and with extra attributes
let user = make('user', 'silly', {name: 'Fred'});
user.toJSON({includeId: true}) // => {id: 5, name: 'Fred', style: 'silly'}
// make user with hats relationship ( hasMany ) composed of a few pre 'made' hats
let hat1 = make('big-hat');
let hat2 = make('big-hat');
let user = make('user', {hats: [hat1, hat2]});
user.toJSON({includeId: true})
// => {id: 6, name: 'User2', style: 'normal', hats: [{id:1, type:"big_hat"},{id:1, type:"big_hat"}]}
// note that hats are polymorphic. if they weren't, the hats array would be a list of ids: [1,2]
// make user with company relationship ( belongsTo ) composed of a pre 'made' company
let company = make('company');
let user = make('user', {company: company});
user.toJSON({includeId: true}) // => {id: 7, name: 'User3', style: 'normal', company: 1}
- check out (user factory): to see 'bob' user and 'with_car' trait
Usage:
import { make, makeList } from 'ember-data-factory-guy';
// Let's say bob is a named type in the
makeList('bob') // makes 0 bob's
makeList('bob', 2) // makes 2 bob's
makeList('bob', 2, 'with_car' , {name: "Dude"})
// makes 2 bob's that have 'with_car' trait and name of "Dude"
// In other words, applies the traits and options to every bob made
makeList('bob', 'with_car', ['with_car',{name: "Dude"}])
// 2 User models with bob attributes, where the first also has 'with_car' trait
// the last has 'with_car' trait and name of "Dude", so you get 2 different bob's
- for building json that you can pass json payload in acceptance tests
- build takes same arguments as make
- can compose relationships with other build/buildList payloads
- to inspect the json use the get() method
Usage:
import { build, buildList } from 'ember-data-factory-guy';
// build basic user with the default attributes in user factory
let json = build('user');
json.get() // => {id: 1, name: 'User1', style: 'normal'}
// build user with default attributes plus those defined as 'admin' in user factory
let json = build('admin');
json.get() // => {id: 2, name: 'Admin', style: 'super'}
// build user with default attributes plus these extra attributes
let json = build('user', {name: 'Fred'});
json.get() // => {id: 3, name: 'Fred', style: 'normal'}
// build admin defined user with extra attributes
let json = build('admin', {name: 'Fred'});
json.get() // => {id: 4, name: 'Fred', style: 'super'}
// build default user with traits and with extra attributes
let json = build('user', 'silly', {name: 'Fred'});
json.get() // => {id: 5, name: 'Fred', style: 'silly'}
// build user with hats relationship ( hasMany ) composed of a few pre 'built' hats
let hat1 = build('big-hat');
let hat2 = build('big-hat');
let json = build('user', {hats: [hat1, hat2]});
// note that hats are polymorphic. if they weren't, the hats array would be a list of ids: [1,2]
json.get() // => {id: 6, name: 'User2', style: 'normal', hats: [{id:1, type:"big_hat"},{id:1, type:"big_hat"}]}
// build user with company relationship ( belongsTo ) composed of a pre 'built' company
let company = build('company');
let json = build('user', {company: company});
json.get() // => {id: 7, name: 'User3', style: 'normal', company: 1}
// build and compose relationships to unlimited degree
let company1 = build('company', {name: 'A Corp'});
let company2 = build('company', {name: 'B Corp'});
let owners = buildList('user', { company:company1 }, { company:company2 });
let buildJson = build('property', { owners });
- Example: what json payload from build looks like
- Although the RESTAdapter is being used, this works the same with ActiveModel or JSONAPI adapters
let json = build('user', 'with_company', 'with_hats');
json // =>
{
user: {
id: 1,
name: 'User1',
company: 1,
hats: [
{type: 'big_hat', id:1},
{type: 'big_hat', id:2}
]
},
companies: [
{id: 1, name: 'Silly corp'}
],
'big-hats': [
{id: 1, type: "BigHat" },
{id: 2, type: "BigHat" }
]
}
- for building json that you can pass json payload in acceptance tests
- buildList takes the same arguments as makeList
- can compose relationships with other build/buildList payloads
- to inspect the json use the get() method
- can use get(index) to get to items in the list
Usage:
import { build, buildList } from 'ember-data-factory-guy';
let owners = buildList('bob', 2); // builds 2 Bob's
let owners = buildList('bob', 2, {name: 'Rob'); // builds 2 Bob's with name of 'Rob'
// builds 2 users, one with name 'Bob' , the next with name 'Rob'
let owners = buildList('user', { name:'Bob' }, { name:'Rob' });
- for inspecting contents of json payload
- get() returns all attributes of top level model
- get(attribute) gives you attribute in top level model
- get(index) gives you the info for hasMany relationship at that index
- get(relationships) gives you just id or type ( if polymorphic )
- better to compose the build relationships by hand if you need more info
- check out user factory: to see 'boblike' and 'adminlike' user traits
let json = build('user');
json.get() //=> {id: 1, name: 'User1', style: 'normal'}
json.get('id') // => 1
let json = buildList('user', 2);
json.get(0) //=> {id: 1, name: 'User1', style: 'normal'}
json.get(1) //=> {id: 2, name: 'User2', style: 'normal'}
let json = buildList('user', 'boblike', 'adminlike');
json.get(0) //=> {id: 1, name: 'Bob', style: 'boblike'}
json.get(1) //=> {id: 2, name: 'Admin', style: 'super'}
- building relationships inline
let json = build('user', 'with_company', 'with_hats');
json.get() //=> {id: 1, name: 'User1', style: 'normal'}
// to get hats (hasMany relationship) info
json.get('hats') //=> [{id: 1, type: "big_hat"},{id: 1, type: "big_hat"}]
// to get company ( belongsTo relationship ) info
json.get('company') //=> {id: 1, type: "company"}
- by composing the relationships you can get the full attributes of those associations
let company = build('company');
let hats = buildList('big-hats');
let user = build('user', {company: company, hats: hats});
user.get() //=> {id: 1, name: 'User1', style: 'normal'}
// to get hats info from hats json
hats.get(0) //=> {id: 1, type: "BigHat", plus .. any other attributes}
hats.get(1) //=> {id: 2, type: "BigHat", plus .. any other attributes}
// to get company info
company.get() //=> {id: 1, type: "Company", name: "Silly corp"}
FactoryGuy handles JSON-API and RESTSerializer out of the box. In case your API doesn't follow either of these conventions, you can still build a custom formatter.
Currently, a custom formatter must implement the following interface:
extractId(modelName, payload)
: Tells FactoryGuy where to find the ID of your payloadconvertForBuild(modelName, payload)
: Transforms a fixture into a JSON payload compatible with your API
// tests/acceptance/my_test.js
import Ember from 'ember';
import FactoryGuy from 'ember-data-factory-guy/factory-guy';
const builderClass = Ember.Object.extend({
extractId(modelName, payload) {
return payload.id;
},
convertForBuild(/* type, payload */) {
return { convert: 'build' };
}
});
FactoryGuy.set('fixtureBuilder', builderClass.create());
- For generating unique attribute values.
- Can be defined:
- In the model definition's sequences hash
- Inline on the attribute
- Values are generated by calling FactoryGuy.generate
FactoryGuy.define('user', {
sequences: {
userName: (num)=> `User${num}`
},
default: {
// use the 'userName' sequence for this attribute
name: FactoryGuy.generate('userName')
}
});
let json = FactoryGuy.build('user');
json.get('name') // => 'User1'
let user = FactoryGuy.make('user');
user.get('name') // => 'User2'
FactoryGuy.define('project', {
special_project: {
title: FactoryGuy.generate((num)=> `Project #${num}`)
},
});
let json = FactoryGuy.build('special_project');
json.get('title') // => 'Project #1'
let project = FactoryGuy.make('special_project');
project.get('title') // => 'Project #2'
- Declare a function for an attribute
- Can reference all other attributes, even id
FactoryGuy.define('user', {
sequences: {
userName: (num)=> `User${num}`
},
default: {
name: FactoryGuy.generate('userName')
},
traits: {
boringStyle: {
style: (f)=> `${f.id} boring `
}
funnyUser: {
style: (f)=> `funny ${f.name}`
}
}
});
let json = FactoryGuy.build('user', 'funny');
json.get('name') // => 'User1'
json.get('style') // => 'funny User1'
let user = FactoryGuy.make('user', 'boring');
user.get('id') // => 2
user.get('style') // => '2 boring'
Note the style attribute was built from a function which depends on the name and the name is a generated attribute from a sequence function
- Used with build, buildList, make, or makeList
- For grouping attributes together
- Can use one or more traits in a row
- The last trait included overrides any values in traits before it
FactoryGuy.define('user', {
traits: {
big: { name: 'Big Guy' }
friendly: { style: 'Friendly' }
}
});
let json = FactoryGuy.build('user', 'big', 'friendly');
json.get('name') // => 'Big Guy'
json.get('style') // => 'Friendly'
let user = FactoryGuy.make('user', 'big', 'friendly');
user.get('name') // => 'Big Guy'
user.get('style') // => 'Friendly'
You can still pass in a hash of options when using traits. This hash of attributes will override any trait attributes or default attributes
let user = FactoryGuy.make('user', 'big', 'friendly', {name: 'Dave'});
user.get('name') // => 'Dave'
user.get('style') // => 'Friendly'
- Can setup belongsTo or hasMany associations in factory definitions
- As inline attribute definition
- With traits
- Can setup belongsTo or hasMany associations manually
// Recall ( from above setup ) that there is a user belongsTo on the Project model
// Also, assume 'user' factory is same as from 'user' factory definition above in
// 'Defining Factories' section
FactoryGuy.define('project', {
project_with_user: {
// create user model with default attributes
user: {}
},
project_with_bob: {
// create user model with custom attributes
user: {name: 'Bob'}
},
project_with_admin: {
// create a named user model with the FactoryGuy.belongsTo helper method
user: FactoryGuy.belongsTo('admin')
}
});
let project = FactoryGuy.make('project_with_admin');
project.get('user.name') // => 'Admin'
project.get('user.style') // => 'super'
You could also accomplish the above with traits:
FactoryGuy.define('project', {
traits: {
with_user: { user: {} },
with_admin: { user: FactoryGuy.belongsTo('admin') }
}
});
let user = FactoryGuy.make('project', 'with_user');
project.get('user').toJSON({includeId: true}) // => {id:1, name: 'Dude', style: 'normal'}
let user = FactoryGuy.make('user');
let project = FactoryGuy.make('project', {user: user});
project.get('user').toJSON({includeId: true}) // => {id:1, name: 'Dude', style: 'normal'}
Note that though you are setting the 'user' belongsTo association on a project, the reverse user hasMany 'projects' association is being setup for you on the user ( for both manual and factory defined belongsTo associations ) as well
user.get('projects.length') // => 1
FactoryGuy.define('user', {
user_with_projects: { projects: FactoryGuy.hasMany('project', 2) }
});
let user = FactoryGuy.make('user_with_projects');
user.get('projects.length') // => 2
You could also accomplish the above with traits:
FactoryGuy.define('project', {
traits: {
with_projects: {
projects: FactoryGuy.hasMany('project', 2)
}
}
});
let user = FactoryGuy.make('user', 'with_projects');
user.get('projects.length') // => 2
let project1 = FactoryGuy.make('project');
let project2 = FactoryGuy.make('project');
let user = FactoryGuy.make('user', {projects: [project1,project2]});
user.get('projects.length') // => 2
// or
let projects = FactoryGuy.makeList('project', 2);
let user = FactoryGuy.make('user', {projects: projects});
user.get('projects.length') // => 2
Note that though you are setting the 'projects' hasMany association on a user, the reverse 'user' belongsTo association is being setup for you on the project ( for both manual and factory defined hasMany associations ) as well
projects.get('firstObject.user') // => user
-
Extending another definition will inherit these sections:
- sequences
- traits
- default attributes
-
Inheritance is fine grained, so in each section, any attribute that is local will take precedence over an inherited one. So you can override some attributes in the default section ( for example ), and inherit the rest
- afterMake
- Uses transient attributes
Assuming the factory-guy model definition defines afterMake function:
FactoryGuy.define('property', {
default: {
name: 'Silly property'
},
// optionally set transient attributes, that will be passed in to afterMake function
transient: {
for_sale: true
},
// The attributes passed to after make will include any optional attributes you
// passed in to make, and the transient attributes defined in this definition
afterMake: function(model, attributes) {
if (attributes.for_sale) {
model.set('name', model.get('name') + '(FOR SALE)');
}
}
}
You would use this to make models like:
Ember.run(function () {
let property = FactoryGuy.make('property');
property.get('name'); // => 'Silly property(FOR SALE)')
let property = FactoryGuy.make('property', {for_sale: false});
property.get('name'); // => 'Silly property')
});
- Easy to create complex scenarios involving multi layered relationships.
- Can use model instances to create relationships for making other models.
Example:
- Setup a scenario where a user has two projects and belongs to a company
let company = make('company');
let user = make('user', {company: company});
let projects = makeList('project', 2, {user: user});
You can use traits to help create the relationships as well, but this strategy allows you to build up complex scenarios in a different way that has it's own benefits.
####cacheOnlyMode
- FactoryGuy.cacheOnlyMode
- Allows you to setup the adapters to prevent them from fetching data with ajax call
- for single models ( find ) you have to put something in the store
- for collections ( findAll ) you don't have to put anything in the store.
- Takes
except
parameter as a list of models you don't want to cache.- These model requests will go to server with ajax call and need to be mocked.
- Allows you to setup the adapters to prevent them from fetching data with ajax call
This is helpful, when:
- you want to set up the test data with make/makeList, and then prevent calls like store.find or findAll from fetching more data, since you have already setup the store with make/makeList data.
- you have an application that starts up and loads data that is not relevant to the test page you are working on.
Usage:
import FactoryGuy, { makeList } from 'ember-data-factory-guy';
import moduleForAcceptance from '../helpers/module-for-acceptance';
moduleForAcceptance('Acceptance | Profiles View');
test("Using FactoryGuy.cacheOnlyMode", function() {
FactoryGuy.cacheOnlyMode();
// the store.find call for the user will go out unless there is a user
// in the store
make('user', {name: 'current'});
// the application starts up and makes calls to findAll a few things, but
// those can be ignored because of the cacheOnlyMode
// for this test I care about just testing profiles
makeList("profile", 2);
visit('/profiles');
andThen(()=> {
// test stuff
});
});
test("Using FactoryGuy.cacheOnlyMode with except", function() {
FactoryGuy.cacheOnlyMode({except: ['profile']});
make('user', {name: 'current'});
// this time I want to allow the ajax call so I can return built json payload
mockFindAll("profile", 2);
visit('/profiles');
andThen(()=> {
// test stuff
});
});
-
FactoryGuy needs to setup the factories before the test run.
- use manualSetup function to set up FactoryGuy in unit/component tests
-
Sample model test (profile-test.js):
- Use 'moduleForModel' ( ember-qunit ), or describeModel ( ember-mocha ) test helper
- manually set up Factory guy
-
Sample component test #1 (single-user-manual-setup-test.js):
- Using 'moduleForComponent' ( ember-qunit ), or describeComponent ( ember-mocha ) helper
- Manually sets up Factory guy ( so it's faster )
import { make, manualSetup } from 'ember-data-factory-guy'; import hbs from 'htmlbars-inline-precompile'; import { test, moduleForComponent } from 'ember-qunit'; moduleForComponent('single-user', 'Integration | Component | single-user (manual setup)', { integration: true, beforeEach: function () { manualSetup(this.container); } }); test("shows user information", function () { let user = make('user', {name: 'Rob'}); this.render(hbs`{{single-user user=user}}`); this.set('user', user); ok(this.$('.name').text().match(user.get('name'))); ok(this.$('.funny-name').text().match(user.get('funnyName'))); });
-
Sample component test #2 (single-user-test.js):
- Using 'moduleForComponent' ( ember-qunit ), or describeComponent ( ember-mocha ) helper
- Starts a new application with startApp() before each test ( slower )
- Uses mockjax
- for mocking the ajax calls made by ember-data.
- this library is installed with ember-data-factory-guy.
- http GET mocks
- mockFind
- mockFindAll
- mockReload
- mockQuery
- mockQueryRecord
- takes modifier method
returns()
for setting the payload responsereturns()
accepts parameters like: json, model, models, id, ids, headers- headers are cumulative so you can add as many as you like
- Example:
let mock = mockFindAll('user').returns({headers: {'X-Man': "Wolverine"}); mock.returns({headers: {'X-Weapon': "Claws"});
- these mocks are are reusable
- so you can simulate making the same ajax call ( url ) and return a different payload
- http POST/PUT/DELETE
- can use method
fails()
to simulate failure, and thensucceeds()
to simulate success- to customize failure, fails method takes optional object with status and errors.
- Example:
let mock = mockFindAll('user').fails({status: 401, errors: {description: "Unauthorized"}});
- Use
mockSetup()
in test setup/beforeEach - Use
mockTeardown()
in test teardown/afterEach - Easiest is to set them up in module-for-acceptance.js:
- For dealing with finding one record of a model type =>
store.find('modelType')
- Can pass in arguments just like you would for make or build
- mockFind( fixture or model name, optional traits, optional attributes object)
- Takes modifier method
returns()
for controlling the response payload- returns( model / json / id )
- Sample acceptance tests using mockFind: user-view-test.js:
Usage:
import { build, make, mockFind } from 'ember-data-factory-guy';
- To return default factory model type ( 'user' in this case )
// mockFind automatically returns json for the modelType ( in this case 'user' )
let mock = mockFind('user');
let userId = mock.get('id');
- Using
returns({json})
to return json object
let user = build('user', 'whacky', {isDude: true});
let mock = mockFind('user').returns({ json: user });
// user.get('id') => 1
// user.get('style') => 'whacky'
// or to acccomplish the same thing with less code
let mock = mockFind('user', 'whacky', {isDude: true});
// mock.get('id') => 1
// mock.get('style') => 'whacky'
let user = mock.get();
// user.id => 1
// user.style => 'whacky'
- Using
returns({model})
to return model instance
let user = make('user', 'whacky', {isDude: false});
let mock = mockFind('user').returns({ model: user });
// user.get('id') => 1
// you can now also user.get('any-computed-property')
// since you have a real model instance
- To reuse the mock
let user2 = build('user', {style: "boring"});
mock.returns({ json: user2 });
// mock.get('id') => 2
- To mock failure case use
fails
method
mockFind('user').fails();
- For dealing with finding all records for a model type =>
store.findAll(modelType)
- Takes same parameters as makeList
- mockFindAll( fixture or model name, optional number, optional traits, optional attributes object)
- Takes modifier method
returns()
for controlling the response payload- returns( models / json / ids )
- Sample acceptance tests using mockFindAll: users-view-test.js
Usage:
import { buildList, makeList, mockFindAll } from 'ember-data-factory-guy';
- To mock and return no results
let mock = mockFindAll('user');
- Using
returns({json})
to return json object
// that has 2 different users:
let users = buildList('user', 'whacky', 'silly');
let mock = mockFindAll('user').returns({ json: users });
let user1 = users.get(0);
let user2 = users.get(1);
// user1.style => 'whacky'
// user2.style => 'silly'
// or to acccomplish the same thing with less code
let mock = mockFindAll('user', 'whacky', 'silly');
let user1 = mock.get(0);
let user2 = mock.get(1);
// user1.style => 'whacky'
// user2.style => 'silly'
- Using
returns({models})
to return model instances
let users = makeList('user', 'whacky', 'silly');
let mock = mockFindAll('user').returns({ models: users });
let user1 = users[0];
// you can now also user1.get('any-computed-property')
// since you have a real model instance
- To reuse the mock and return different payload
let users2 = buildList('user', 3);
mock.returns({ json: user2 });
- To mock failure case use
fails()
method
mockFindAll('user').fails();
- To handle reloading a model
- Pass in a record ( or a typeName and id )
Usage:
- Passing in a record / model instance
let profile = make('profile');
mockReload(profile);
// will stub a call to reload that profile
profile.reload()
- Mocking a failed reload
mockReload('profile', 1).fails();
- For dealing with querying for all records for a model type =>
store.query(modelType, params)
- Takes modifier method
returns()
for controlling the response payload - returns( models / json / ids )
- Takes modifier method
- Takes modifier methods for matching the query params - withParams( object )
- Sample acceptance tests using mockQuery: user-search-test.js
Usage:
import FactoryGuy, { make, build, buildList, mockQuery } from 'ember-data-factory-guy';
let store = FactoryGuy.store;
// This simulates a query that returns no results
mockQuery('user', {age: 10});
store.query('user', {age: 10}}).then((userInstances) => {
/// userInstances will be empty
})
- with returns( models )
// Create model instances
let users = makeList('user', 2, 'with_hats');
mockQuery('user', {name:'Bob', age: 10}).returns({models: users});
store.query('user', {name:'Bob', age: 10}}).then((models)=> {
// models are the same as the users array
});
- with returns ( json )
// Create json with buildList
let users = buildList('user', 2, 'with_hats');
mockQuery('user', {name:'Bob', age: 10}).returns({json: users});
store.query('user', {name:'Bob', age: 10}}).then((models)=> {
// these models were created from the users json
});
- with returns( ids )
// Create list of models
let users = buildList('user', 2, 'with_hats');
let user1 = users.get(0);
mockQuery('user', {name:'Bob', age: 10}).returns({ids: [user1.id]});
store.query('user', {name:'Bob', age: 10}}).then(function(models) {
// models will be one model and it will be user1
});
- For dealing with querying for one record for a model type =>
store.queryRecord(modelType, params)
- Takes modifier method
returns()
for controlling the response payload - returns( model / json / id )
- Takes modifier method
- Takes modifier methods for matching the query params - withParams( object )
Usage:
import FactoryGuy, { make, build, mockQueryRecord } from 'ember-data-factory-guy';
let store = FactoryGuy.store;
// This simulates a query that returns no results
mockQueryRecord('user', {age: 10});
store.queryRecord('user', {age: 10}}).then((userInstance) => {
/// userInstance will be empty
})
- with returns( models )
// Create model instances
let user = make('user');
mockQueryRecord('user', {name:'Bob', age: 10}).returns({model: user});
store.queryRecord('user', {name:'Bob', age: 10}}).then((model)=> {
// model is the same as the user you made
});
- with returns ( json )
// Create json with buildList
let user = build('user');
mockQueryRecord('user', {name:'Bob', age: 10}).returns({json: user});
store.queryRecord('user', {name:'Bob', age: 10}}).then((model)=> {
// user model created from the user json
});
- with returns( ids )
// Create list of models
let user = build('user', 'with_hats');
mockQueryRecord('user', {name:'Bob', age: 10}).returns({id: user.get('id')});
store.queryRecord('user', {name:'Bob', age: 10}}).then(function(model) {
// model will be one model and it will be user1
});
-
Use chainable methods to build the response
- match
- Attributes that must be in request json
- returns
- Attributes to include in response json
- fails
- Request will fail
- Takes a hash of options:
- status - HTTP status code, defaults to 500.
- response - error response message, or an errors hash for 422 status
- match
-
Need to wrap tests using mockCreate with: Ember.run.function() { 'your test' })
Note
Any attributes in match will be added to the response json automatically, so you don't need to include them in the returns hash as well.
If you match on a belongsTo association, you don't have to include that in the returns hash.
Realistically, you will have code in a view action or controller action that will create the record, and setup any associations.
// most actions that create a record look something like this:
action: {
addProject: function (user) {
let name = this.$('button.project-name').val();
let store = this.get('controller.store');
store.createRecord('project', {name: name, user: user}).save();
}
}
In this case, you are are creating a 'project' record with a specific name, and belonging to a particular user. To mock this createRecord call here are a few ways to do this using chainable methods.
Usage:
import { mockCreate } from 'ember-data-factory-guy';
// Simplest case
// Don't care about a match just handle createRecord for any project
mockCreate('project');
// Matching some attributes
mockCreate('project').match({name: "Moo"});
// Match all attributes
mockCreate('project').match({name: "Moo", user: user});
// Exactly matching attributes, and returning extra attributes
mockCreate('project')
.match({name: "Moo", user: user})
.returns({created_at: new Date()});
- mocking a failed create
// Mocking failure case is easy with chainable methods, just use #fails
mockCreate('project').match({name: "Moo"}).fails();
// Can optionally add a status code and/or errors to the response
mockCreate('project').fails({status: 422, response: {errors: {name: ['Moo bad, Bahh better']}}});
store.createRecord('project', {name: "Moo"}).save(); //=> fails
- mockUpdate(model)
- Single argument ( the model instance that will be updated )
- mockUpdate(modelType, id)
- Two arguments: modelType ( like 'profile' ) , and the profile id that will updated
- Use chainable methods to help build response:
- fails
- Request will fail
- Optional arguments ( status and response text )
- succeeds
- Update should succeed, this is the default behavior
- Can even use this after an
fails
call to simulate failure with invalid properties and then success after valid ones.
- fails
- Need to wrap tests using mockUpdate with: Ember.run.function() { 'your test' })
Usage:
import { make, mockUpdate } from 'ember-data-factory-guy';
let profile = make('profile');
// Pass in the model that will be updated ( if you have it available )
mockUpdate(profile);
// If the model is not available, pass in the modelType and the id of
// the model that will be updated
mockUpdate('profile', 1);
profile.set('description', 'good value');
profile.save() //=> will succeed
- mocking a failed update
let profile = make('profile');
// set the succeed flag to 'false'
mockUpdate('profile', profile.id).fails({status: 422, response: 'Invalid data'});
// or
mockUpdate(profile).fails({status: 422, response: 'Invalid data'});
profile.set('description', 'bad value');
profile.save() //=> will fail
mocking a failed update and retry with succees
let profile = make('profile');
let mockUpdate = mockUpdate(profile);
mockUpdate.fails({status: 422, response: 'Invalid data'});
profile.set('description', 'bad value');
profile.save() //=> will fail
// After setting valid value
profile.set('description', 'good value');
// Now expecting success
mockUpdate.succeeds();
// Try that update again
profile.save() //=> will succeed!
- Need to wrap tests using mockDelete with: Ember.run.function() { 'your test' })
Usage:
import { make, mockDelete } from 'ember-data-factory-guy';
let profile = make('profile');
mockDelete('profile', profile.id);
profile.destroyRecord() // => will succeed
- mocking a failed delete
mockDelete('profile', profile.id, false);
profile.destroyRecord() // => will fail
Sample Acceptance test (user-view-test.js):
// file: tests/acceptance/user-view-test.js
import { make, mockCreate, mockSetup, mockTeardown } from 'ember-data-factory-guy';
import moduleForAcceptance from '../helpers/module-for-acceptance';
moduleForAcceptance('Acceptance | User View', {
beforeEach: function () {
// mockSetup sets $.mockjaxSettings response time to zero ( speeds up tests )
mockSetup();
},
afterEach: function () {
// mockTeardown calls $.mockjax.clear() which resets all the mockjax handlers
mockTeardown();
}
});
test("Creates new project", function () {
let user = build('user', 'with_projects'); // build user payload
visit('/user/'+ user.get('id'));
andThen(function () {
let newProjectName = "Gonzo Project";
fillIn('input.project-name', newProjectName);
// Remember, this is for handling an exact match, if you did not care about
// matching attributes, you could just do: mockCreate('project')
mockCreate('project', {match: {name: newProjectName, user: user}});
/**
Let's say that clicking this 'button.add-project', triggers action in the view to
create project record and looks something like this:
actions: {
addProject: function (user) {
let name = this.$('input.project-name').val();
let store = this.get('controller.store');
store.createRecord('project', {name: name, user: user}).save();
}
*/
click('button:contains(Add New User)');
andThen(function () {
let newProjectDiv = find('li.project:contains(' + newProjectName + ')');
ok(newProjectDiv[0] !== undefined);
});
});
});