Ember Data Factory Guy
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.
NEW starting with v3.8
- jquery is no longer required and fetch adapter is used with ember-data
- you can still use jquery if you want to
- if you are addon author using factory guy set up your application adapter like this
NEW starting with v3.2.1
- You can setup data AND links for your async relationship Check it out
NEW You can use factory guy in ember-twiddle
NEW If using new style of ember-qunit acceptance tests with setupApplicationTest
check out demo here: user-view-test.js:
NEW starting with v2.13.27
- get attributes for factory defined models with
attributesFor
NEW starting with v2.13.24
- manualSetup streamlined to
manualSetup(this)
NEW and Improved starting with v2.13.22
Older but still fun things
Why is FactoryGuy so awesome
- Since you're using ember data, you don't need to create any ORM like things
- You don't need to add any files to recreate the relationships in your models
- Any custom methods like: serialize / serializeAttribute / keyForAttribute etc... in a serializer will be used automatically
- If you set up custom methods like: buildURL / urlForFindRecord in an adapter, they will be used automatically
- You have no config file with tons of spew, because you declare all the mocks and make everything declaratively in the test
- You can push models and their complex relationships directly to the store
Questions / Get in Touch
Visit the EmberJS Community #e-factory-guy Slack channel
Contents
How it works
- You create factories for your models.
- put them in the
tests/factories
directory
- Use these factories to create models for 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
Installation
ember install ember-data-factory-guy
( 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 )
Upgrading
- remove ember-data-factory-guy from
package.json
npm prune
ember install ember-data-factory-guy
( for the latest release )
Setup
In the following examples, assume the models look like this:
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')
});
Hat = DS.Model.extend({
type: DS.attr('string'),
user: DS.belongsTo('user')
});
BigHat = Hat.extend();
SmallHat = Hat.extend();
Defining Factories
- A factory has a name and a set of attributes.
- The name should match the model type name. So, for the model
User
, the factory name would be user
- Create factory files in the
tests/factories
directory. - Can use generators to create the outline of a factory file:
ember generate factory user
This will create a factory in a file named user.js
in the tests/factories
directory.
Standard models
import FactoryGuy from 'ember-data-factory-guy';
FactoryGuy.define('user', {
default: {
style: 'normal',
name: 'Dude'
},
admin: {
style: 'super',
name: 'Admin'
}
});
- If you are using an attribute named
type
and this is not a polymorphic model, use the option
polymorphic: false
in your definition
FactoryGuy.define('cat', {
polymorphic: false,
default: {
type: 'Cute',
name: (f)=> `Cat ${f.id}`
}
});
Polymorphic models
- Define each polymorphic model in its own typed definition
- The attribute named
type
is used to hold the model name - May want to extend the parent factory here (see extending other definitions)
import FactoryGuy from 'ember-data-factory-guy';
FactoryGuy.define('small-hat', {
default: {
type: 'SmallHat'
}
})
import FactoryGuy from 'ember-data-factory-guy';
FactoryGuy.define('big-hat', {
default: {
type: 'BigHat'
}
})
In other words, don't do this:
import FactoryGuy from 'ember-data-factory-guy';
FactoryGuy.define('hat', {
default: {},
small-hat: {
type: 'SmallHat'
},
big-hat: {
type: 'BigHat'
}
})
Sequences
- 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
Declaring sequences in sequences hash
FactoryGuy.define('user', {
sequences: {
userName: (num)=> `User${num}`
},
default: {
name: FactoryGuy.generate('userName')
}
});
let first = FactoryGuy.build('user');
first.get('name')
let second = FactoryGuy.make('user');
second.get('name')
Declaring an inline sequence on attribute
FactoryGuy.define('project', {
special_project: {
title: FactoryGuy.generate((num)=> `Project #${num}`)
},
});
let json = FactoryGuy.build('special_project');
json.get('title')
let project = FactoryGuy.make('special_project');
project.get('title')
Inline Functions
- Declare a function for an attribute
- The fixture is passed as parameter so you can reference
all other attributes, even id
FactoryGuy.define('user', {
default: {
name: (f)=> `User${f.id}`
},
traits: {
boring: {
style: (f)=> `${f.id} boring`
},
funny: {
style: (f)=> `funny ${f.name}`
}
}
});
let json = FactoryGuy.build('user', 'funny');
json.get('name')
json.get('style')
let user = FactoryGuy.make('user', 'boring');
user.get('id')
user.get('style')
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
Traits
- Used with
attributesFor , build/buildList , make/makeList
- For grouping attributes together
- Can use one or more traits
- Each trait overrides any values defined in traits before it in the argument list
- traits can be functions ( this is mega powerful )
FactoryGuy.define('user', {
traits: {
big: { name: 'Big Guy' },
friendly: { style: 'Friendly' },
bfg: { name: 'Big Friendly Giant', style: 'Friendly' }
}
});
let user = FactoryGuy.make('user', 'big', 'friendly');
user.get('name')
user.get('style')
let giant = FactoryGuy.make('user', 'big', 'bfg');
user.get('name')
user.get('style')
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')
user.get('style')
Using traits as functions
import FactoryGuy from 'ember-data-factory-guy';
FactoryGuy.define("project", {
default: {
title: (f) => `Project ${f.id}`
},
traits: {
medium: (f) => {
f.title = `Medium Project ${f.id}`
},
goofy: (f) => {
f.title = `Goofy ${f.title}`
}
withUser: (f) => {
f.user = FactoryGuy.make('user')
}
}
});
So, when you make / build a project like:
let project = make('project', 'medium');
project.get('title');
let project2 = build('project', 'goofy');
project2.get('title');
let project3 = build('project', 'withUser');
project3.get('user.name');
Your trait function assigns the title as you described in the function
Associations
Setup belongsTo associations in Factory Definitions
- using traits are the best practice
FactoryGuy.define('project', {
traits: {
withUser: { user: {} },
withAdmin: { user: FactoryGuy.belongsTo('user', 'admin') },
withManagerLink(f) {
f.links = {manager: `/projects/${f.id}/manager`}
}
}
});
let user = make('project', 'withUser');
project.get('user').toJSON({includeId: true})
user = make('user', 'withManagerLink');
user.belongsTo('manager').link();
Setup belongsTo associations manually
See FactoryGuy.build
/FactoryGuy.buildList
for more ideas
let user = make('user');
let project = make('project', {user});
project.get('user').toJSON({includeId: true})
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')
Setup hasMany associations in the Factory Definition
- using traits are the best practice
- Do not create
hasMany
records via the default
section of the factory definition. Prefer traits to set up such associations. Creating them via the default
section is known to cause some undefined behavior when using the makeNew
API.
FactoryGuy.define('user', {
traits: {
withProjects: {
projects: FactoryGuy.hasMany('project', 2)
},
withPropertiesLink(f) {
f.links = {properties: `/users/${f.id}/properties`}
}
}
});
let user = make('user', 'withProjects');
user.get('projects.length')
user = make('user', 'withPropertiesLink');
user.hasMany('properties').link();
You could also setup a custom named user definition:
FactoryGuy.define('user', {
userWithProjects: { projects: FactoryGuy.hasMany('project', 2) }
});
let user = make('userWithProjects');
user.get('projects.length')
Setup hasMany associations manually
See FactoryGuy.build
/FactoryGuy.makeList
for more ideas
let project1 = make('project');
let project2 = make('project');
let user = make('user', {projects: [project1, project2]});
user.get('projects.length')
let projects = makeList('project', 2);
let user = make('user', {projects});
user.get('projects.length')
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')
Special tips for links
- The links syntax changed as of ( v3.2.1 )
- What you see below is the new syntax
- You can setup data AND links for your async relationship
- Need special care with multiple traits setting links
FactoryGuy.define('user', {
traits: {
withCompanyLink(f): {
f.links = Object.assign({company: `/users/${f.id}/company`}, f.links);
},
withPropertiesLink(f) {
f.links = Object.assign({properties: `/users/${f.id}/properties`}, f.links);
}
}
});
let company = make('company')
let user = make('user', 'withCompanyLink', 'withPropertiesLink', {company});
user.hasMany('properties').link();
user.belongsTo('company').link();
user.get('company.content')
user.belongsTo('company').reload()
user = make('user', {links: {properties: '/users/1/properties'}});
Extending Other Definitions
- 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
There is a sample Factory using inheritance here: big-group.js
Transient Attributes
- Use transient attributes to build a fixture
- Pass in any attribute you like to build a fixture
- Usually helps you to build some other attribute
- These attributes will be removed when fixture is done building
- Can be used in
make
/makeList
/build
/buildList
Let's say you have a model and a factory like this:
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
dogNumber: attr('string'),
sound: attr('string')
});
import FactoryGuy from 'ember-data-factory-guy';
const defaultVolume = "Normal";
FactoryGuy.define('dog', {
default: {
dogNumber: (f)=> `Dog${f.id}`,
sound: (f) => `${f.volume || defaultVolume} Woof`
},
});
Then to build the fixture:
let dog2 = build('dog', { volume: 'Soft' });
dog2.get('sound');
Callbacks
afterMake
- Uses transient attributes
- Unfortunately the model will fire 'onload' event before this
afterMake
is called.
- So all data will not be setup by then if you rely on
afterMake
to finish by the
time onload
is called. - In this case, just use transient attributes without the
afterMake
Assuming the factory-guy model definition defines afterMake
function:
FactoryGuy.define('property', {
default: {
name: 'Silly property'
},
transient: {
for_sale: true
},
afterMake: function(model, attributes) {
if (attributes.for_sale) {
model.set('name', model.get('name') + '(FOR SALE)');
}
}
}
You would use this to make models like:
run(function () {
let property = FactoryGuy.make('property');
property.get('name');
let property = FactoryGuy.make('property', {for_sale: false});
property.get('name');
});
Remember to import the run
function with import { run } from "@ember/runloop"
;
Using Factories
FactoryGuy.attributesFor
- returns attributes ( for now no relationship info )
FactoryGuy.make
- push model instances into store
FactoryGuy.makeNew
- Create a new model instance but doesn't load it to the store
FactoryGuy.makeList
- Loads zero to many model instances into the store
FactoryGuy.build
- Builds json in accordance with the adapter's specifications
FactoryGuy.buildList
- Builds json with a list of zero or more items in accordance with the adapter's 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
- Can setup links for async relationships with
build
/buildList
or make
/makeList
FactoryGuy.attributesFor
- nice way to get attibutes for a factory without making a model or payload
- same arguments as make/build
- no id is returned
- no relationship info returned ( yet )
import { attributesFor } from 'ember-data-factory-guy';
attributesFor('user', 'silly', {name: 'Fred'});
FactoryGuy.make
- Loads a model instance into the store
- makes a fragment hash ( if it is a model fragment )
- can compose relationships with other
FactoryGuy.make
/FactoryGuy.makeList
- can add relationship links to payload
import { make } from 'ember-data-factory-guy';
let user = make('user');
user.toJSON({includeId: true});
let user = make('admin');
user.toJSON({includeId: true});
let user = make('user', {name: 'Fred'});
user.toJSON({includeId: true});
let user = make('admin', {name: 'Fred'});
user.toJSON({includeId: true});
let user = make('user', 'silly', {name: 'Fred'});
user.toJSON({includeId: true});
let hat1 = make('big-hat');
let hat2 = make('big-hat');
let user = make('user', {hats: [hat1, hat2]});
user.toJSON({includeId: true})
let company = make('company');
let user = make('user', {company: company});
user.toJSON({includeId: true})
let user = make('user', {properties: {links: '/users/1/properties'}});
let user = make('user', {company: {links: '/users/1/company'}});
let object = make('name');
FactoryGuy.makeNew
- Same api as
FactoryGuy.make
- except that the model will be a newly created record with no id
FactoryGuy.makeList
Usage:
import { make, makeList } from 'ember-data-factory-guy';
makeList('user', 'bob')
makeList('user', 'bob', 2)
makeList('user', 'bob', 2, 'with_car', {name: "Dude"})
makeList('user', 'bob', 'with_car', ['with_car', {name: "Dude"}])
FactoryGuy.build
- for building json that you can pass as json payload in acceptance tests
- takes the same arguments as
FactoryGuy.make
- can compose relationships with other
FactoryGuy.build
/FactoryGuy.buildList
payloads - can add relationship links to payload
- takes serializer for model into consideration
- to inspect the json use the
get
method - use the
add
method
- to include extra sideloaded data to the payload
- to include meta data
- REMEMBER, all relationships will be automatically sideloaded,
so you don't need to add them with the
add()
method
Usage:
import { build, buildList } from 'ember-data-factory-guy';
let json = build('user');
json.get()
let json = build('admin');
json.get()
let json = build('user', {name: 'Fred'});
json.get()
let json = build('admin', {name: 'Fred'});
json.get()
let json = build('user', 'silly', {name: 'Fred'});
json.get()
let hat1 = build('big-hat');
let hat2 = build('big-hat');
let json = build('user', {hats: [hat1, hat2]});
json.get()
let company = build('company');
let json = build('user', {company});
json.get()
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 });
let user = build('user', {properties: {links: '/users/1/properties'}});
let user = build('user', {company: {links: '/users/1/company'}});
- Example of 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" }
]
}
FactoryGuy.buildList
- for building json that you can pass as json payload in acceptance tests
- takes the same arguments as
FactoryGuy.makeList
- can compose relationships with other
build
/buildList
payloads - takes serializer for model into consideration
- to inspect the json use the
get()
method
- can use
get(index)
to get an individual item from the list
- use the
add
method
- to add extra sideloaded data to the payload =>
.add(payload)
- to add meta data =>
.add({meta})
Usage:
import { build, buildList } from 'ember-data-factory-guy';
let bobs = buildList('bob', 2);
let bobs = buildList('bob', 2, {name: 'Rob'});
let users = buildList('user', { name:'Bob' }, { name:'Rob' });
let users = buildList('user', 'boblike', 'adminlike');
let users = buildList('user', ['boblike', { style: 'stoner' }], ['adminlike', {style: 'square'}]);
Using add()
method
- when you need to add more json to a payload
- will be sideloaded
- only JSONAPI, and REST based serializers can do sideloading
- so DRFSerializer and JSONSerializer users can not use this feature
- you dont need to use json key as in:
build('user').add({json: batMan})
- you can just add the payload directly as:
build('user').add(batMan)
Usage:
let batMan = build('bat_man');
let userPayload = build('user').add(batMan);
userPayload = {
user: {
id: 1,
name: 'User1',
style: "normal"
},
'super-heros': [
{
id: 1,
name: "BatMan",
type: "SuperHero"
}
]
};
- when you want to add meta data to payload
- only JSONAPI, and REST based and serializers and DRFSerializer can handle meta data
- so JSONSerializer users can not use this feature ( though this might be a bug on my part )
Usage:
let json1 = buildList('profile', 2).add({ meta: { previous: '/profiles?page=1', next: '/profiles?page=3' } });
let json2 = buildList('profile', 2).add({ meta: { previous: '/profiles?page=2', next: '/profiles?page=4' } });
mockQuery('profile', {page: 2}).returns({ json: json1 });
mockQuery('profile', {page: 3}).returns({ json: json2 });
store.query('profile', {page: 2}).then((records)=>
store.query('profile', {page: 3}).then((records)=>
Using get()
method
- for inspecting contents of json payload
get()
returns all attributes of top level modelget(attribute)
gives you an attribute from the top level modelget(index)
gives you the info for a hasMany relationship at that indexget(relationships)
gives you just the 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()
json.get('id')
let json = buildList('user', 2);
json.get(0)
json.get(1)
let json = buildList('user', 'boblike', 'adminlike');
json.get(0)
json.get(1)
- building relationships inline
let json = build('user', 'with_company', 'with_hats');
json.get()
json.get('hats')
json.get('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 , hats});
user.get()
hats.get(0)
hats.get(1)
company.get()
Using in Other Environments
-
You can set up scenarios for your app that use all your factories from tests updating config/environment.js
.
-
NOTE: Do not use settings in the test
environment. Factories are enabled
by default for the test
environment and setting the flag tells factory-guy to load the app/scenarios
files which are not needed for using factory-guy in testing. This will result in errors being generated if
the app/scenarios files do not exist.
if (environment === 'development') {
ENV.factoryGuy = { useScenarios: true };
ENV.locationType = 'auto';
ENV.rootURL = '/';
}
if (environment === 'production') {
ENV.factoryGuy = {enabled: true, useScenarios: true};
ENV.locationType = 'auto';
ENV.rootURL = '/';
}
-
Place your scenarios in the app/scenarios
directory
- Start by creating at least a
scenarios/main.js
file since this is the starting point - Your scenario classes should inherit from
Scenario
class - A scenario class should declare a run method where you do things like:
- include other scenarios
- you can compose scenarios like a symphony of notes
- make your data or mock your requests using the typical Factory Guy methods
- these methods are all built into scenario classes so you don't have to import them
import {Scenario} from 'ember-data-factory-guy';
import Users from './users';
Scenario.settings({
logLevel: 1,
});
export default class extends Scenario {
run() {
this.include([Users]);
this.mockFindAll('products', 3);
this.mock({
type: 'POST',
url: '/api/v1/users/sign_in',
responseText: { token:"0123456789-ab" }
});
}
}
import {Scenario} from 'ember-data-factory-guy';
export default class extends Scenario {
run() {
this.mockFindAll('user', 'boblike', 'normal');
this.mockDelete('user');
}
}
Ember Data Model Fragments
As of 2.5.2 you can create factories which contain ember-data-model-fragments. Setting up your fragments is easy and follows the same process as setting up regular factories. The mapping between fragment types and their associations are like so:
Fragment Type | Association |
---|
fragment | FactoryGuy.belongsTo |
fragmentArray | FactoryGuy.hasMany |
array | [] |
For example, say we have the following Employee
model which makes use of the fragment
, fragmentArray
and array
fragment types.
export default Model.extend({
name: fragment('name'),
phoneNumbers: fragmentArray('phone-number')
})
export default Fragment.extend({
titles: array('string'),
firstName: attr('string'),
lastName: attr('string')
});
export default Fragment.extend({
number: attr('string')
type: attr('string')
});
A factory for this model and its fragments would look like so:
FactoryGuy.define('employee', {
default: {
name: FactoryGuy.belongsTo('name'),
phoneNumbers: FactoryGuy.hasMany('phone-number')
}
});
FactoryGuy.define('name', {
default: {
titles: ['Mr.', 'Dr.'],
firstName: 'Jon',
lastName: 'Snow'
}
});
FactoryGuy.define('phone-number', {
default: {
number: '123-456-789',
type: 'home'
}
});
To set up associations manually ( and not necessarily in a factory ), you should do:
let phoneNumbers = makeList('phone-numbers', 2);
let employee = make('employee', { phoneNumbers });
let phoneNumbers = buildList('phone-numbers', 2).get();
let employee = build('employee', { phoneNumbers }).get();
For a more detailed example of setting up fragments have a look at:
Creating Factories in Addons
If you are making an addon with factories and you want the factories available to Ember apps using your addon, place the factories in test-support/factories
instead of tests/factories
. They should be available both within your addon and in Ember apps that use your addon.
Ember Django Adapter
- available since 2.6.1
- everything is setup automatically
- sideloading is not supported in
DRFSerializer
so all relationships should either
- be set as embedded with
DS.EmbeddedRecordsMixin
if you want to use build
/buildList
- or use
make
/makeList
and in your mocks, and return models instead of json:
let projects = makeList('projects', 2);
let user = make('user', { projects });
mockFindRecord('user').returns({model: user});
- using
fails()
with errors hash is not working reliably
- so you can always just
mockWhatever(args).fails()
Custom API formats
FactoryGuy handles JSON-API / RESTSerializer / JSONSerializer out of the box.
In case your API doesn't follow any of these conventions, you can still make a custom fixture builder
or modify the FixtureConverters
and JSONPayload
classes that exist.
- before I launch into the details, let me know if you need this hookup and I
can guide you to a solution, since the use cases will be rare and varied.
FactoryGuy.cacheOnlyMode
- Allows you to setup the adapters to prevent them from fetching data with ajax calls
- for single models (
findRecord
) 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 the server with ajax calls and will need to be mocked
This is helpful, when:
- you want to set up the test data with
make
/makeList
, and then prevent
calls like store.findRecord
or store.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", async function() {
FactoryGuy.cacheOnlyMode();
make('user', {name: 'current'});
makeList("profile", 2);
await visit('/profiles');
});
test("Using FactoryGuy.cacheOnlyMode with except", async function() {
FactoryGuy.cacheOnlyMode({except: ['profile']});
make('user', {name: 'current'});
mockFindAll("profile", 2);
await visit('/profiles');
});
Testing models, controllers, components
-
FactoryGuy needs to setup the factories before the test run.
-
By default, you only need to call manualSetup(this)
in unit/component/acceptance tests
-
Or you can use the new setupFactoryGuy(hooks) method if your using the new qunit style tests
- Sample usage: (works the same in any type of test)
import { setupFactoryGuy } from "ember-data-factory-guy";
module('Acceptance | User View', function(hooks) {
setupApplicationTest(hooks);
setupFactoryGuy(hooks);
test("blah blah", async function(assert) {
await visit('work');
assert.ok('bah was spoken');
});
});
-
Sample model test: profile-test.js
- Use
moduleForModel
( ember-qunit ), or describeModel
( ember-mocha ) test helper - manually set up FactoryGuy
-
Sample component test: single-user-test.js
- Using
moduleForComponent
( ember-qunit ), or describeComponent
( ember-mocha ) helper - manually sets up FactoryGuy
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);
}
});
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')));
});
Acceptance Tests
- For using new style of ember-qunit with
setupApplicationTest
check out demo here: user-view-test.js:
Using mock methods
-
Uses pretender
- for mocking the ajax calls made by ember-data
- pretender library is installed with FactoryGuy
-
http GET mocks
-
http POST/PUT/DELETE
-
Custom mocks (http GET/POST/PUT/DELETE)
-
Use method fails()
to simulate failure
-
Use method succeeds()
to simulate success
- Only used if the mock was set to fail with
fails()
and you want to set the
mock to succeed to simulate a successful retry
-
Use property timesCalled
to verify how many times the ajax call was mocked
- works when you are using
mockQuery
, mockQueryRecord
, mockFindAll
, mockReload
, or mockUpdate
mockFindRecord
will always be at most 1 since it will only make ajax call
the first time, and then the store will use cache the second time- Example:
const mock = mockQueryRecord('company', {}).returns({ json: build('company') });
FactoryGuy.store.queryRecord('company', {}).then(()=> {
FactoryGuy.store.queryRecord('company', {}).then(()=> {
mock.timesCalled
});
});
-
Use method disable()
to temporarily disable the mock. You can re-enable
the disabled mock using enable()
.
-
Use method destroy()
to completely remove the mock handler for the mock.
The isDestroyed
property is set to true
when the mock is destroyed.
setup
Using fails method
let errors401 = {errors: {description: "Unauthorized"}};
let mock = mockFindAll('user').fails({status: 401, response: errors401});
let errors422 = {errors: {name: "Name too short"}};
let mock = mockFindRecord('profile').fails({status: 422, response: errors422});
let errorsMine = {errors: [{detail: "Name too short", title: "I am short"}]};
let mock = mockFindRecord('profile').fails({status: 422, response: errorsMine, convertErrors: false});
mockFindRecord
- For dealing with finding one record of a model type =>
store.findRecord('modelType', id)
- Can pass in arguments just like you would for
make
or build
mockFindRecord
( fixture or model name, optional traits, optional attributes object)
- Takes modifier method
returns()
for controlling the response payload
- returns( model / json / id )
- Takes modifier method
adapterOptions()
for setting adapterOptions ( get passed to urlForFindRecord ) - Sample acceptance tests using
mockFindRecord
: user-view-test.js:
Usage:
import { build, make, mockFindRecord } from 'ember-data-factory-guy';
- To return default factory model type ( 'user' in this case )
let mock = mockFindRecord('user');
let userId = mock.get('id');
- Using
returns({json})
to return json object
let user = build('user', 'whacky', {isDude: true});
let mock = mockFindRecord('user').returns({ json: user });
let mock = mockFindRecord('user', 'whacky', {isDude: true});
let user = mock.get();
- Using
returns({model})
to return model instance
let user = make('user', 'whacky', {isDude: false});
let mock = mockFindRecord('user').returns({ model: user });
- Simper way to return a model instance
let user = make('user', 'whacky', {isDude: false});
let mock = mockFindRecord(user);
let user2 = build('user', {style: "boring"});
mock.returns({ json: user2 });
- To mock failure case use
fails
method
mockFindRecord('user').fails();
- To mock failure when you have a model already
let profile = make('profile');
mockFindRecord(profile).fails();
let mock = mockFindRecord('user').adapterOptions({friendly: true});
urlForFindRecord(id, modelName, snapshot) {
if (snapshot && snapshot.adapterOptions) {
let { adapterOptions } = snapshot;
}
}
mockFindAll
- 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 )
- Takes modifier method
adapterOptions()
for setting adapterOptions ( get passed to urlForFindAll )
- used just as in mockFindRecord ( see example there )
- 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
let users = buildList('user', 'whacky', 'silly');
let mock = mockFindAll('user').returns({ json: users });
let user1 = users.get(0);
let user2 = users.get(1);
let mock = mockFindAll('user', 'whacky', 'silly');
let user1 = mock.get(0);
let user2 = mock.get(1);
- Using
returns({models})
to return model instances
let users = makeList('user', 'whacky', 'silly');
let mock = mockFindAll('user').returns({ models: users });
let user1 = users[0];
- 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();
mockReload
- 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);
profile.reload()
- Using
returns({attrs})
to return new attributes
let profile = make('profile', { description: "whatever" });
mockReload(profile).returns({ attrs: { description: "moo" } });
profile.reload();
- Using
returns({json})
to return all new attributes
let profile = make('profile', { description: "tomatoes" });
let profileAllNew = build('profile', { id: profile.get('id'), description: "potatoes" }
mockReload(profile).returns({ json: profileAllNew });
profile.reload();
mockReload('profile', 1).fails();
mockQuery
- 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 methods for matching the query params
-
withParams( object )
- withSomeParams( 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;
mockQuery('user', {age: 10});
store.query('user', {age: 10}}).then((userInstances) => {
})
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)=> {
});
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)=> {
});
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) {
});
- withParams() / withSomeParams()
let users = buildList('user', 2, 'with_hats');
let user1 = users.get(0);
mock = mockQuery('user').returns({ids: [user1.id]});
mock.withParams({name:'Bob', age: 10})
store.query('user', {name:'Bob', age: 10}}).then(function(models) {
});
store.query('user', {name:'Bob', age: 10, hair: 'brown'}})
mock.withSomeParams({name:'Bob'})
store.query('user', {name:'Bob', age: 10}})
store.query('user', {name:'Bob', age: 10, hair: 'brown'}})
mockQueryRecord
- 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 methods for matching the query params
- withParams( object )
Usage:
import FactoryGuy, { make, build, mockQueryRecord } from 'ember-data-factory-guy';
let store = FactoryGuy.store;
mockQueryRecord('user', {age: 10});
store.queryRecord('user', {age: 10}}).then((userInstance) => {
})
let user = make('user');
mockQueryRecord('user', {name:'Bob', age: 10}).returns({model: user});
store.queryRecord('user', {name:'Bob', age: 10}}).then((model)=> {
});
let user = build('user');
mockQueryRecord('user', {name:'Bob', age: 10}).returns({json: user});
store.queryRecord('user', {name:'Bob', age: 10}}).then((model)=> {
});
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) {
});
mockCreate
- Use chainable methods to build the response
- match: takes a hash with attributes or a matching function
- attributes that must be in request json
- These will be added to the response json automatically, so
you don't need to include them in the returns hash.
- If you match on a
belongsTo
association, you don't have to include that in
the returns hash either ( same idea )
- a function that can be used to perform an arbitrary match against the request
json, returning
true
if there is a match, false
otherwise.
- returns
- attributes ( including relationships ) to include in response json
- Need to import
run
from @ember/runloop
and wrap tests using mockCreate
with: run(function() { 'your test' })
Realistically, you will have code in a view action or controller action that will
create the record, and setup any associations.
action: {
addProject: function (user) {
let name = this.$('button.project-name').val();
this.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 { makeNew, mockCreate } from 'ember-data-factory-guy';
mockCreate('project');
let project = makeNew('project');
mockCreate(project);
mockCreate('project').match({name: "Moo"});
mockCreate('project').match({name: "Moo", user: user});
mockCreate('project').match(requestData => requestData.name === 'Moo');
mockCreate('project')
.match({name: "Moo", user: user})
.returns({created_at: new Date()});
let person = build('super-hero');
mockCreate('outfit').returns({attrs: { person }});
let outfits = buildList('outfit', 2);
mockCreate('super-hero').returns({attrs: { outfits }});
mockCreate('project').match({name: "Moo"}).fails();
mockCreate('project').fails({status: 422, response: {errors: {name: ['Moo bad, Bahh better']}}});
store.createRecord('project', {name: "Moo"}).save();
mockUpdate
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:
match
: takes a hash with attributes or a matching function
- attributes with values that must be present on the model you are updating
- a function that can be used to perform an arbitrary match against the request
json, returning
true
if there is a match, false
otherwise.
- returns
- attributes ( including relationships ) to include in response json
- Need to import
run
from @ember/runloop
and wrap tests using mockUpdate
with: run(function() { 'your test' })
Usage:
import { make, mockUpdate } from 'ember-data-factory-guy';
let profile = make('profile');
mockUpdate(profile);
mockUpdate('profile', 1);
profile.set('description', 'good value');
profile.save()
let outfit = make('outfit');
let person = build('super-hero');
outfit.set('name','outrageous');
mockUpdate(outfit).returns({attrs: { person }});
outfit.save();
let superHero = make('super-hero');
let outfits = buildList('outfit', 2, {name:'bell bottoms'});
superHero.set('style','laid back');
mockUpdate(superHero).returns({attrs: { outfits }});
superHero.save();
let profile = make('profile');
profile.set('name', "woo");
let mock = mockUpdate(profile).match({name: "moo"});
profile.save();
let profile = make('profile');
profile.set('name', "woo");
let mock = mockUpdate(profile).match((requestBody) => {
return requestBody.data.attributes.name === "moo"
});
profile.save();
profile.set('name', "moo");
profile.save();
mock.match({name: "woo"});
profile.save();
let profile = make('profile');
mockUpdate('profile', profile.id).fails({status: 422, response: 'Invalid data'});
mockUpdate(profile).fails({status: 422, response: 'Invalid data'});
profile.set('description', 'bad value');
profile.save()
mocking a failed update and retry with success
let profile = make('profile');
let mockUpdate = mockUpdate(profile);
mockUpdate.fails({status: 422, response: 'Invalid data'});
profile.set('description', 'bad value');
profile.save()
profile.set('description', 'good value');
mockUpdate.succeeds();
profile.save()
mockDelete
- Need to import
run
from @ember/runloop
and wrap tests using mockDelete
with: run(function() { 'your test' })
- To handle deleting a model
- Pass in a record ( or a typeName and id )
Usage:
- Passing in a record / model instance
import { make, mockDelete } from 'ember-data-factory-guy';
let profile = make('profile');
mockDelete(profile);
profile.destroyRecord()
- Passing in a model typeName and id
import { make, mockDelete } from 'ember-data-factory-guy';
let profile = make('profile');
mockDelete('profile', profile.id);
profile.destroyRecord()
- Passing in a model typeName
import { make, mockDelete } from 'ember-data-factory-guy';
let profile1 = make('profile');
let profile2 = make('profile');
mockDelete('profile');
profile1.destroyRecord()
profile2.destroyRecord()
mockDelete(profile).fails();
mock
Well, you have read about all the other mock*
methods, but what if you have
endpoints that do not use Ember Data? Well, mock
is for you.
- mock({type, url, responseText, status})
- type: The HTTP verb (
GET
, POST
, etc.) Defaults to GET
- url: The endpoint URL you are trying to mock
- responseText: This can be whatever you want to return, even a JavaScript object
- status: The status code of the response. Defaults to
200
Usage:
import { mock } from 'ember-data-factory-guy';
this.mock({ url: '/users' });
- Returning a JavaScript object
import { mock } from 'ember-data-factory-guy';
this.mock({
type: 'POST',
url: '/users/sign_in',
responseText: { token: "0123456789-ab" }
});
Pretender
The addon uses Pretender to mock the requests. It exposes the functions getPretender
and setPretender
to respectively get the Pretender server for the current test or set it. For instance, you can use pretender's passthrough feature to ignore data URLs:
import { getPretender } from 'ember-data-factory-guy';
getPretender().get('data:*', getPretender().passthrough);
Tips and Tricks
Tip 1: Fun with makeList
/buildList
and traits
- This is probably the funnest thing in FactoryGuy, if you're not using this
syntax yet, you're missing out.
let json = buildList('widget', 'square', 'round', ['round','broken']);
let widgets = makeList('widget', 'square', 'round', ['round','broken']);
let [squareWidget, roundWidget, roundBrokenWidget] = widgets;
- you just built/made 3 different widgets from traits ('square', 'round', 'broken')
- the first will have the square trait
- the second will have the round trait
- the third will have both round and broken trait
Tip 2: Building static / fixture like data into the factories.
- States are the classic case. There is a state model, and there are 50 US states.
- You could use a strategy to get them with traits like this:
import FactoryGuy from 'ember-data-factory-guy';
FactoryGuy.define('state', {
traits: {
NY: { name: "New York", id: "NY" },
NJ: { name: "New Jersey", id: "NJ" },
CT: { name: "Connecticut", id: "CT" }
}
});
let [ny, nj, ct] = makeList('state', 'ny', 'nj', 'ct');
- Or you could use a strategy to get them like this:
import FactoryGuy from 'ember-data-factory-guy';
const states = [
{ name: "New York", id: "NY" },
{ name: "New Jersey", id: "NJ" },
{ name: "Connecticut", id: "CT" }
... blah .. blah .. blah
];
FactoryGuy.define('state', {
default: {
id: FactoryGuy.generate((i)=> states[i-1].id),
name: FactoryGuy.generate((i)=> states[i-1].name)
}
});
let states = makeList('state', 3);
Tip 3: Using Scenario class in tests
- encapsulate data interaction in a scenario class
- sets up data
- has helper methods to retrieve data
- similar to how page objects abstract away the interaction with a page/component
Example:
import Ember from 'ember';
import {Scenario} from 'ember-data-factory-guy';
export default class extends Scenario {
run() {
this.createGroups();
}
createGroups() {
this.permissionGroups = this.makeList('permission-group', 3);
}
groupNames() {
return this.permissionGroups.mapBy('name').sort();
}
}
import page from '../pages/admin';
import Scenario from '../scenarios/admin';
describe('Admin View', function() {
let scenario;
beforeEach(function() {
scenario = new Scenario();
scenario.run();
});
describe('group', function() {
beforeEach(function() {
page.visitGroups();
});
it('shows all groups', function() {
expect(page.groups.names).to.arrayEqual(scenario.groupNames());
});
});
});
Tip 4: Testing mocks ( async testing ) in unit tests
- Two ways to handle asyncronous test
Tip 5: Testing model's custom serialize()
method
- The fact that you can match on attributes in
mockUpdate
and mockCreate
means
that you can test a custom serialize()
method in a model serializer
export default DS.RESTSerializer.extend({
serialize: function(snapshot, options) {
var json = this._super(snapshot, options);
let honorificName = [snapshot.record.get('name'), 'san'].join('-');
json.name = honorificName;
return json;
}
});
let person = make('person', {name: "Daniel"});
mockUpdate(person).match({name: "Daniel-san"});
person.save();
- You could also test
serialize()
method in a simpler way by doing this:
let person = make('person', {name: "Daniel"});
let json = person.serialize();
assert.equal(json.name, 'Daniel-san');
Releasing new versions
- npm version (patch|minor|major)
- npm publish
- git push --tags
ChangeLog