Installations
npm install ember-data-factory-guy
Releases
Developer
adopted-ember-addons
Developer Guide
Module System
CommonJS
Min. Node Version
>= 8
Typescript Support
No
Node Version
14.15.0
NPM Version
7.10.0
Statistics
301 Stars
1,920 Commits
137 Forks
17 Watching
20 Branches
96 Contributors
Updated on 19 Nov 2024
Languages
JavaScript (98.74%)
Handlebars (0.83%)
HTML (0.42%)
CSS (0.01%)
Total Downloads
Cumulative downloads
Total Downloads
3,617,456
Last day
12.6%
1,495
Compared to previous day
Last week
-0.1%
6,830
Compared to previous week
Last month
7.1%
29,320
Compared to previous month
Last year
16.2%
314,991
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
Dependencies
6
Dev Dependencies
25
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
- Using Scenarios
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
- Traits can be functions Check it out
Older but still fun things
- Support for ember-data-model-fragment usage is baked in since v2.5.0
- Support for ember-django-adapter usage is fried in since v2.6.1
- Support for adding meta data to payloads for use with ember-infinity ie. => pagination
- Support for adding headers to payloads
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
- Installation
- Upgrading
- Setup
- Defining Factories
- Using Factories
- Using in Development, Production or other environments
- Ember Data Model Fragments
- Creating Factories in Addons
- Ember Django Adapter
- Custom API formats
- Testing models, controllers, components
- Acceptance Tests
- Pretender
- Tips and Tricks
- Changelog
How it works
- You create factories for your models.
- put them in the
tests/factories
directory
- put them in the
- 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:
1 // standard models 2 class User extends Model { 3 @attr('string') name 4 @attr('string') style 5 @hasMany('project') projects 6 @hasMany('hat', {polymorphic: true}) hats 7 } 8 9 class Project extends Model { 10 @attr('string') title 11 @belongsTo('user') user 12 } 13 14 // polymorphic models 15 class Hat extends Model { 16 @attr('string') type 17 @belongsTo('user') user 18 } 19 20 class BigHat extends Hat {}; 21 class SmallHat extends Hat {};
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 beuser
- 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 nameduser.js
in thetests/factories
directory.
Standard models
-
Sample full blown factory:
user.js
-
Brief sample of a factory definition:
1 2 // file tests/factories/user.js 3 import FactoryGuy from 'ember-data-factory-guy'; 4 5 FactoryGuy.define('user', { 6 // Put default 'user' attributes in the default section 7 default: { 8 style: 'normal', 9 name: 'Dude' 10 }, 11 // Create a named 'user' with custom attributes 12 admin: { 13 style: 'super', 14 name: 'Admin' 15 } 16 }); 17
- If you are using an attribute named
type
and this is not a polymorphic model, use the optionpolymorphic: false
in your definition
1// file: tests/factories/cat.js 2FactoryGuy.define('cat', { 3 polymorphic: false, // manually flag this model as NOT polymorphic 4 default: { 5 // usually, an attribute named 'type' is for polymorphic models, but the defenition 6 // is set as NOT polymorphic, which allows this type to work as attibute 7 type: 'Cute', 8 name: (f)=> `Cat ${f.id}` 9 } 10});
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)
1 2 // file tests/factories/small-hat.js 3 import FactoryGuy from 'ember-data-factory-guy'; 4 5 FactoryGuy.define('small-hat', { 6 default: { 7 type: 'SmallHat' 8 } 9 }) 10 11 // file tests/factories/big-hat.js 12 import FactoryGuy from 'ember-data-factory-guy'; 13 14 FactoryGuy.define('big-hat', { 15 default: { 16 type: 'BigHat' 17 } 18 }) 19
In other words, don't do this:
1 // file tests/factories/hat.js 2 import FactoryGuy from 'ember-data-factory-guy'; 3 4 FactoryGuy.define('hat', { 5 default: {}, 6 small-hat: { 7 type: 'SmallHat' 8 }, 9 big-hat: { 10 type: 'BigHat' 11 } 12 }) 13
Sequences
- For generating unique attribute values.
- Can be defined:
- In the model definition's
sequences
hash - Inline on the attribute
- In the model definition's
- Values are generated by calling
FactoryGuy.generate
Declaring sequences in sequences hash
1 2 FactoryGuy.define('user', { 3 sequences: { 4 userName: (num)=> `User${num}` 5 }, 6 7 default: { 8 // use the 'userName' sequence for this attribute 9 name: FactoryGuy.generate('userName') 10 } 11 }); 12 13 let first = FactoryGuy.build('user'); 14 first.get('name') // => 'User1' 15 16 let second = FactoryGuy.make('user'); 17 second.get('name') // => 'User2' 18
Declaring an inline sequence on attribute
1 2 FactoryGuy.define('project', { 3 special_project: { 4 title: FactoryGuy.generate((num)=> `Project #${num}`) 5 }, 6 }); 7 8 let json = FactoryGuy.build('special_project'); 9 json.get('title') // => 'Project #1' 10 11 let project = FactoryGuy.make('special_project'); 12 project.get('title') // => 'Project #2' 13
Inline Functions
- Declare a function for an attribute
- The fixture is passed as parameter so you can reference all other attributes, even id
1 2 FactoryGuy.define('user', { 3 default: { 4 // Don't need the userName sequence, since the id is almost 5 // always a sequential number, and you can use that. 6 // f is the fixture being built as the moment for this factory 7 // definition, which has the id available 8 name: (f)=> `User${f.id}` 9 }, 10 traits: { 11 boring: { 12 style: (f)=> `${f.id} boring` 13 }, 14 funny: { 15 style: (f)=> `funny ${f.name}` 16 } 17 } 18 }); 19 20 let json = FactoryGuy.build('user', 'funny'); 21 json.get('name') // => 'User1' 22 json.get('style') // => 'funny User1' 23 24 let user = FactoryGuy.make('user', 'boring'); 25 user.get('id') // => 2 26 user.get('style') // => '2 boring' 27
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 )
1 2 FactoryGuy.define('user', { 3 traits: { 4 big: { name: 'Big Guy' }, 5 friendly: { style: 'Friendly' }, 6 bfg: { name: 'Big Friendly Giant', style: 'Friendly' } 7 } 8 }); 9 10 let user = FactoryGuy.make('user', 'big', 'friendly'); 11 user.get('name') // => 'Big Guy' 12 user.get('style') // => 'Friendly' 13 14 let giant = FactoryGuy.make('user', 'big', 'bfg'); 15 user.get('name') // => 'Big Friendly Giant' - name defined in the 'bfg' trait overrides the name defined in the 'big' trait 16 user.get('style') // => 'Friendly' 17 18
You can still pass in a hash of options when using traits. This hash of attributes will override any trait attributes or default attributes
1 2 let user = FactoryGuy.make('user', 'big', 'friendly', {name: 'Dave'}); 3 user.get('name') // => 'Dave' 4 user.get('style') // => 'Friendly' 5
Using traits as functions
1import FactoryGuy from 'ember-data-factory-guy'; 2 3FactoryGuy.define("project", { 4 default: { 5 title: (f) => `Project ${f.id}` 6 }, 7 traits: { 8 // this trait is a function 9 // note that the fixure is passed in that will have 10 // default attributes like id at a minimum and in this 11 // case also a title ( `Project 1` ) which is default 12 medium: (f) => { 13 f.title = `Medium Project ${f.id}` 14 }, 15 goofy: (f) => { 16 f.title = `Goofy ${f.title}` 17 } 18 withUser: (f) => { 19 // NOTE: you're not using FactoryGuy.belongsTo as you would 20 // normally in a fixture definition 21 f.user = FactoryGuy.make('user') 22 } 23 } 24});
So, when you make / build a project like:
1let project = make('project', 'medium'); 2project.get('title'); //=> 'Medium Project 1' 3 4let project2 = build('project', 'goofy'); 5project2.get('title'); //=> 'Goofy Project 2' 6 7let project3 = build('project', 'withUser'); 8project3.get('user.name'); //=> 'User 1'
Your trait function assigns the title as you described in the function
Associations
-
Can setup belongsTo or hasMany associations in factory definitions
- As inline attribute definition
- With traits
- as links for async relationships
-
Can setup belongsTo or hasMany associations manually
- With
FactoryGuy.build
/FactoryGuy.buildList
andFactoryGuy.make
/FactoryGuy.makeList
- Can compose relationships to any level
- When setting up manually do not mix
build
andmake
- you eitherbuild
JSON in every levels of associations ormake
objects.build
is taking serializer into account for every model which means that output frombuild
might be different than expected input defined in factory inmake
.
- With
-
Special tips for links
Setup belongsTo associations in Factory Definitions
- using traits are the best practice
1 FactoryGuy.define('project', { 2 3 traits: { 4 withUser: { user: {} }, 5 withAdmin: { user: FactoryGuy.belongsTo('user', 'admin') }, 6 withManagerLink(f) { // f is the fixture being created 7 f.links = {manager: `/projects/${f.id}/manager`} 8 } 9 } 10 }); 11 12 let user = make('project', 'withUser'); 13 project.get('user').toJSON({includeId: true}) // => {id:1, name: 'Dude', style: 'normal'} 14 15 user = make('user', 'withManagerLink'); 16 user.belongsTo('manager').link(); // => "/projects/1/manager" 17
Setup belongsTo associations manually
See FactoryGuy.build
/FactoryGuy.buildList
for more ideas
1 let user = make('user'); 2 let project = make('project', {user}); 3 4 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
1 user.get('projects.length') // => 1
Setup hasMany associations in the Factory Definition
- using traits are the best practice
- Do not create
hasMany
records via thedefault
section of the factory definition. Prefer traits to set up such associations. Creating them via thedefault
section is known to cause some undefined behavior when using themakeNew
API.
1 2 FactoryGuy.define('user', { 3 traits: { 4 withProjects: { 5 projects: FactoryGuy.hasMany('project', 2) 6 }, 7 withPropertiesLink(f) { // f is the fixture being created 8 f.links = {properties: `/users/${f.id}/properties`} 9 } 10 } 11 }); 12 13 let user = make('user', 'withProjects'); 14 user.get('projects.length') // => 2 15 16 user = make('user', 'withPropertiesLink'); 17 user.hasMany('properties').link(); // => "/users/1/properties"
You could also setup a custom named user definition:
1 FactoryGuy.define('user', { 2 3 userWithProjects: { projects: FactoryGuy.hasMany('project', 2) } 4 5 }); 6 7 let user = make('userWithProjects'); 8 user.get('projects.length') // => 2
Setup hasMany associations manually
See FactoryGuy.build
/FactoryGuy.makeList
for more ideas
1 let project1 = make('project'); 2 let project2 = make('project'); 3 let user = make('user', {projects: [project1, project2]}); 4 user.get('projects.length') // => 2 5 6 // or 7 let projects = makeList('project', 2); 8 let user = make('user', {projects}); 9 user.get('projects.length') // => 2 10
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
1 projects.get('firstObject.user') // => 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
1 2 FactoryGuy.define('user', { 3 traits: { 4 withCompanyLink(f): { 5 // since you can assign many different links with different traits, 6 // you should Object.assign so that you add to the links hash rather 7 // than set it directly ( assuming you want to use this feature ) 8 f.links = Object.assign({company: `/users/${f.id}/company`}, f.links); 9 }, 10 withPropertiesLink(f) { 11 f.links = Object.assign({properties: `/users/${f.id}/properties`}, f.links); 12 } 13 } 14 }); 15 16 // setting links with traits 17 let company = make('company') 18 let user = make('user', 'withCompanyLink', 'withPropertiesLink', {company}); 19 user.hasMany('properties').link(); // => "/users/1/properties" 20 user.belongsTo('company').link(); // => "/users/1/company" 21 // the company async relationship has a company AND link to fetch it again 22 // when you reload that relationship 23 user.get('company.content') // => company 24 user.belongsTo('company').reload() // would use that link "/users/1/company" to reload company 25 26 // you can also set traits with your build/buildList/make/makeList options 27 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:
1 2 // app/models/dog.js 3 import Model from 'ember-data/model'; 4 import attr from 'ember-data/attr'; 5 6 export default class Dog extends Model{ 7 @attr('string') dogNumber 8 @attr('string') sound 9 } 10 11 // tests/factories/dog.js 12 import FactoryGuy from 'ember-data-factory-guy'; 13 14 const defaultVolume = "Normal"; 15 16 FactoryGuy.define('dog', { 17 default: { 18 dogNumber: (f)=> `Dog${f.id}`, 19 sound: (f) => `${f.volume || defaultVolume} Woof` 20 }, 21 });
Then to build the fixture:
1 let dog2 = build('dog', { volume: 'Soft' }); 2 3 dog2.get('sound'); //=> `Soft Woof`
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 timeonload
is called. - In this case, just use transient attributes without the
afterMake
- So all data will not be setup by then if you rely on
Assuming the factory-guy model definition defines afterMake
function:
1 FactoryGuy.define('property', { 2 default: { 3 name: 'Silly property' 4 }, 5 6 // optionally set transient attributes, that will be passed in to afterMake function 7 transient: { 8 for_sale: true 9 }, 10 11 // The attributes passed to after make will include any optional attributes you 12 // passed in to make, and the transient attributes defined in this definition 13 afterMake: function(model, attributes) { 14 if (attributes.for_sale) { 15 model.set('name', model.get('name') + '(FOR SALE)'); 16 } 17 } 18 }
You would use this to make models like:
1 run(function () { 2 3 let property = FactoryGuy.make('property'); 4 property.get('name'); // => 'Silly property(FOR SALE)') 5 6 let property = FactoryGuy.make('property', {for_sale: false}); 7 property.get('name'); // => 'Silly property') 8 }); 9
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
- RESTAdapter (assume this adapter being used in most of the following examples)
- ActiveModelAdapter
- JSONAPIAdapter
- DrfAdapter (Ember Django Adapter)
- 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
ormake
/makeList
- By passing in other objects you've made with
- Can setup links for async relationships with
build
/buildList
ormake
/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 )
1 2 import { attributesFor } from 'ember-data-factory-guy'; 3 4 // make a user with certain traits and options 5 attributesFor('user', 'silly', {name: 'Fred'}); // => { name: 'Fred', style: 'silly'} 6
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
1 2 import { make } from 'ember-data-factory-guy'; 3 4 // make a user with the default attributes in user factory 5 let user = make('user'); 6 user.toJSON({includeId: true}); // => {id: 1, name: 'User1', style: 'normal'} 7 8 // make a user with the default attributes plus those defined as 'admin' in the user factory 9 let user = make('admin'); 10 user.toJSON({includeId: true}); // => {id: 2, name: 'Admin', style: 'super'} 11 12 // make a user with the default attributes plus these extra attributes provided in the optional hash 13 let user = make('user', {name: 'Fred'}); 14 user.toJSON({includeId: true}); // => {id: 3, name: 'Fred', style: 'normal'} 15 16 // make an 'admin' user with these extra attributes 17 let user = make('admin', {name: 'Fred'}); 18 user.toJSON({includeId: true}); // => {id: 4, name: 'Fred', style: 'super'} 19 20 // make a user with a trait ('silly') plus these extra attributes provided in the optional hash 21 let user = make('user', 'silly', {name: 'Fred'}); 22 user.toJSON({includeId: true}); // => {id: 5, name: 'Fred', style: 'silly'} 23 24 // make a user with a hats relationship ( hasMany ) composed of pre-made hats 25 let hat1 = make('big-hat'); 26 let hat2 = make('big-hat'); 27 let user = make('user', {hats: [hat1, hat2]}); 28 user.toJSON({includeId: true}) 29 // => {id: 6, name: 'User2', style: 'normal', hats: [{id:1, type:"big_hat"},{id:1, type:"big_hat"}]} 30 // note that hats are polymorphic. if they weren't, the hats array would be a list of ids: [1,2] 31 32 // make a user with a company relationship ( belongsTo ) composed of a pre-made company 33 let company = make('company'); 34 let user = make('user', {company: company}); 35 user.toJSON({includeId: true}) // => {id: 7, name: 'User3', style: 'normal', company: 1} 36 37 // make user with links to async hasMany properties 38 let user = make('user', {properties: {links: '/users/1/properties'}}); 39 40 // make user with links to async belongsTo company 41 let user = make('user', {company: {links: '/users/1/company'}}); 42 43 // for model fragments you get an object 44 let object = make('name'); // => {firstName: 'Boba', lastName: 'Fett'} 45
FactoryGuy.makeNew
- Same api as
FactoryGuy.make
- except that the model will be a newly created record with no id
FactoryGuy.makeList
- check out (user factory): to see 'bob' user and 'with_car' trait
Usage:
1 2 import { make, makeList } from 'ember-data-factory-guy'; 3 4 // Let's say bob is a named type in the user factory 5 makeList('user', 'bob') // makes 0 bob's 6 7 makeList('user', 'bob', 2) // makes 2 bob's 8 9 makeList('user', 'bob', 2, 'with_car', {name: "Dude"}) 10 // makes 2 bob users with the 'with_car' trait and name of "Dude" 11 // In other words, applies the traits and options to every bob made 12 13 makeList('user', 'bob', 'with_car', ['with_car', {name: "Dude"}]) 14 // makes 2 users with bob attributes. The first also has the 'with_car' trait and the 15 // second has the 'with_car' trait and name of "Dude", so you get 2 different users 16 17
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:
1 2 import { build, buildList } from 'ember-data-factory-guy'; 3 4 // build a basic user with the default attributes from the user factory 5 let json = build('user'); 6 json.get() // => {id: 1, name: 'User1', style: 'normal'} 7 8 // build a user with the default attributes plus those defined as 'admin' in the user factory 9 let json = build('admin'); 10 json.get() // => {id: 2, name: 'Admin', style: 'super'} 11 12 // build a user with the default attributes with extra attributes 13 let json = build('user', {name: 'Fred'}); 14 json.get() // => {id: 3, name: 'Fred', style: 'normal'} 15 16 // build the admin defined user with extra attributes 17 let json = build('admin', {name: 'Fred'}); 18 json.get() // => {id: 4, name: 'Fred', style: 'super'} 19 20 // build default user with traits and with extra attributes 21 let json = build('user', 'silly', {name: 'Fred'}); 22 json.get() // => {id: 5, name: 'Fred', style: 'silly'} 23 24 // build user with hats relationship ( hasMany ) composed of a few pre 'built' hats 25 let hat1 = build('big-hat'); 26 let hat2 = build('big-hat'); 27 let json = build('user', {hats: [hat1, hat2]}); 28 // note that hats are polymorphic. if they weren't, the hats array would be a list of ids: [1,2] 29 json.get() // => {id: 6, name: 'User2', style: 'normal', hats: [{id:1, type:"big_hat"},{id:1, type:"big_hat"}]} 30 31 // build user with company relationship ( belongsTo ) composed of a pre 'built' company 32 let company = build('company'); 33 let json = build('user', {company}); 34 json.get() // => {id: 7, name: 'User3', style: 'normal', company: 1} 35 36 // build and compose relationships to unlimited degree 37 let company1 = build('company', {name: 'A Corp'}); 38 let company2 = build('company', {name: 'B Corp'}); 39 let owners = buildList('user', { company:company1 }, { company:company2 }); 40 let buildJson = build('property', { owners }); 41 42 // build user with links to async hasMany properties 43 let user = build('user', {properties: {links: '/users/1/properties'}}); 44 45 // build user with links to async belongsTo company 46 let user = build('user', {company: {links: '/users/1/company'}}); 47
- Example of what json payload from build looks like
- Although the RESTAdapter is being used, this works the same with ActiveModel or JSONAPI adapters
1 2 let json = build('user', 'with_company', 'with_hats'); 3 json // => 4 { 5 user: { 6 id: 1, 7 name: 'User1', 8 company: 1, 9 hats: [ 10 {type: 'big_hat', id:1}, 11 {type: 'big_hat', id:2} 12 ] 13 }, 14 companies: [ 15 {id: 1, name: 'Silly corp'} 16 ], 17 'big-hats': [ 18 {id: 1, type: "BigHat" }, 19 {id: 2, type: "BigHat" } 20 ] 21 } 22
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
- can use
- use the
add
method- to add extra sideloaded data to the payload =>
.add(payload)
- to add meta data =>
.add({meta})
- to add extra sideloaded data to the payload =>
Usage:
1 import { build, buildList } from 'ember-data-factory-guy'; 2 3 let bobs = buildList('bob', 2); // builds 2 Bob's 4 5 let bobs = buildList('bob', 2, {name: 'Rob'}); // builds 2 Bob's with name of 'Rob' 6 7 // builds 2 users, one with name 'Bob' , the next with name 'Rob' 8 let users = buildList('user', { name:'Bob' }, { name:'Rob' }); 9 10 // builds 2 users, one with 'boblike' and the next with name 'adminlike' features 11 // NOTE: you don't say how many to make, because each trait is making new user 12 let users = buildList('user', 'boblike', 'adminlike'); 13 14 // builds 2 users: 15 // one 'boblike' with stoner style 16 // and the next 'adminlike' with square style 17 // NOTE: how you are grouping traits and attributes for each one by wrapping them in array 18 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)
- will be sideloaded
Usage:
1 let batMan = build('bat_man'); 2 let userPayload = build('user').add(batMan); 3 4 userPayload = { 5 user: { 6 id: 1, 7 name: 'User1', 8 style: "normal" 9 }, 10 'super-heros': [ 11 { 12 id: 1, 13 name: "BatMan", 14 type: "SuperHero" 15 } 16 ] 17 };
- 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:
1 let json1 = buildList('profile', 2).add({ meta: { previous: '/profiles?page=1', next: '/profiles?page=3' } }); 2 let json2 = buildList('profile', 2).add({ meta: { previous: '/profiles?page=2', next: '/profiles?page=4' } }); 3 4 mockQuery('profile', {page: 2}).returns({ json: json1 }); 5 mockQuery('profile', {page: 3}).returns({ json: json2 }); 6 7 store.query('profile', {page: 2}).then((records)=> // first 2 from json1 8 store.query('profile', {page: 3}).then((records)=> // second 2 from json2 9
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
1 let json = build('user'); 2 json.get() //=> {id: 1, name: 'User1', style: 'normal'} 3 json.get('id') // => 1 4 5 let json = buildList('user', 2); 6 json.get(0) //=> {id: 1, name: 'User1', style: 'normal'} 7 json.get(1) //=> {id: 2, name: 'User2', style: 'normal'} 8 9 let json = buildList('user', 'boblike', 'adminlike'); 10 json.get(0) //=> {id: 1, name: 'Bob', style: 'boblike'} 11 json.get(1) //=> {id: 2, name: 'Admin', style: 'super'}
- building relationships inline
1 2 let json = build('user', 'with_company', 'with_hats'); 3 json.get() //=> {id: 1, name: 'User1', style: 'normal'} 4 5 // to get hats (hasMany relationship) info 6 json.get('hats') //=> [{id: 1, type: "big_hat"},{id: 1, type: "big_hat"}] 7 8 // to get company ( belongsTo relationship ) info 9 json.get('company') //=> {id: 1, type: "company"} 10
- by composing the relationships you can get the full attributes of those associations
1 2 let company = build('company'); 3 let hats = buildList('big-hats'); 4 5 let user = build('user', {company , hats}); 6 user.get() //=> {id: 1, name: 'User1', style: 'normal'} 7 8 // to get hats info from hats json 9 hats.get(0) //=> {id: 1, type: "BigHat", plus .. any other attributes} 10 hats.get(1) //=> {id: 2, type: "BigHat", plus .. any other attributes} 11 12 // to get company info 13 company.get() //=> {id: 1, type: "Company", name: "Silly corp"} 14
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 thetest
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.1 // file: config/environment.js 2 // in development you don't have to set enabled to true since that is default 3 if (environment === 'development') { 4 ENV.factoryGuy = { useScenarios: true }; 5 ENV.locationType = 'auto'; 6 ENV.rootURL = '/'; 7 } 8 9 // or 10 11 if (environment === 'production') { 12 ENV.factoryGuy = {enabled: true, useScenarios: true}; 13 ENV.locationType = 'auto'; 14 ENV.rootURL = '/'; 15 } 16
-
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
- include other scenarios
1 // file: app/scenarios/main.js 2 import {Scenario} from 'ember-data-factory-guy'; 3 import Users from './users'; 4 5 // Just for fun, set the log level ( to 1 ) and see all FactoryGuy response info in console 6 Scenario.settings({ 7 logLevel: 1, // 1 is the max for now, default is 0 8 }); 9 10 export default class extends Scenario { 11 run() { 12 this.include([Users]); // include other scenarios 13 this.mockFindAll('products', 3); // mock some finds 14 this.mock({ 15 type: 'POST', 16 url: '/api/v1/users/sign_in', 17 responseText: { token:"0123456789-ab" } 18 }); // mock a custom endpoint 19 } 20 }
1 // file: app/scenarios/users.js 2 import {Scenario} from 'ember-data-factory-guy'; 3 4 export default class extends Scenario { 5 run() { 6 this.mockFindAll('user', 'boblike', 'normal'); 7 this.mockDelete('user'); 8 } 9 }
- Start by creating at least a
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.
1// Employee model 2export default class Employee extends Model { 3 @fragment('name') name 4 @fragmentArray('phone-number') phoneNumbers 5} 6 7// Name fragment 8export default class Name extends Fragment { 9 @array('string') titles 10 @attr('string') firstName 11 @attr('string') lastName 12} 13 14// Phone Number fragment 15export default class PhoneNumber extends Fragment { 16 @attr('string') number 17 @attr('string') type 18}
A factory for this model and its fragments would look like so:
1// Employee factory 2FactoryGuy.define('employee', { 3 default: { 4 name: FactoryGuy.belongsTo('name'), //fragment 5 phoneNumbers: FactoryGuy.hasMany('phone-number') //fragmentArray 6 } 7}); 8 9// Name fragment factory 10FactoryGuy.define('name', { 11 default: { 12 titles: ['Mr.', 'Dr.'], //array 13 firstName: 'Jon', 14 lastName: 'Snow' 15 } 16}); 17 18// Phone number fragment factory 19FactoryGuy.define('phone-number', { 20 default: { 21 number: '123-456-789', 22 type: 'home' 23 } 24});
To set up associations manually ( and not necessarily in a factory ), you should do:
1let phoneNumbers = makeList('phone-numbers', 2); 2let employee = make('employee', { phoneNumbers }); 3 4// OR 5 6let phoneNumbers = buildList('phone-numbers', 2).get(); 7let employee = build('employee', { phoneNumbers }).get();
For a more detailed example of setting up fragments have a look at:
- model test employee test.
- acceptance test employee-view-test.
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 usebuild
/buildList
- or use
make
/makeList
and in your mocks, and return models instead of json:
- be set as embedded with
1 let projects = makeList('projects', 2); // put projects in the store 2 let user = make('user', { projects }); // attach them to user 3 mockFindRecord('user').returns({model: user}); // now the mock will return a user that has projects
- using
fails()
with errors hash is not working reliably- so you can always just
mockWhatever(args).fails()
- so you can always just
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
- for single models (
- 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 likestore.findRecord
orstore.findAll
from fetching more data, since you have already setup the store withmake
/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:
1import FactoryGuy, { makeList } from 'ember-data-factory-guy'; 2import moduleForAcceptance from '../helpers/module-for-acceptance'; 3 4moduleForAcceptance('Acceptance | Profiles View'); 5 6test("Using FactoryGuy.cacheOnlyMode", async function() { 7 FactoryGuy.cacheOnlyMode(); 8 // the store.findRecord call for the user will go out unless there is a user 9 // in the store 10 make('user', {name: 'current'}); 11 // the application starts up and makes calls to findAll a few things, but 12 // those can be ignored because of the cacheOnlyMode 13 14 // for this test I care about just testing profiles 15 makeList("profile", 2); 16 17 await visit('/profiles'); 18 19 // test stuff 20}); 21 22test("Using FactoryGuy.cacheOnlyMode with except", async function() { 23 FactoryGuy.cacheOnlyMode({except: ['profile']}); 24 25 make('user', {name: 'current'}); 26 27 // this time I want to allow the ajax call so I can return built json payload 28 mockFindAll("profile", 2); 29 30 await visit('/profiles'); 31 32 // test stuff 33});
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)
1import { setupFactoryGuy } from "ember-data-factory-guy"; 2 3module('Acceptance | User View', function(hooks) { 4 setupApplicationTest(hooks); 5 setupFactoryGuy(hooks); 6 7 test("blah blah", async function(assert) { 8 await visit('work'); 9 assert.ok('bah was spoken'); 10 }); 11});
-
-
Sample model test: profile-test.js
- Use
moduleForModel
( ember-qunit ), ordescribeModel
( ember-mocha ) test helper - manually set up FactoryGuy
- Use
-
Sample component test: single-user-test.js
- Using
moduleForComponent
( ember-qunit ), ordescribeComponent
( ember-mocha ) helper - manually sets up FactoryGuy
1import { make, manualSetup } from 'ember-data-factory-guy'; 2import hbs from 'htmlbars-inline-precompile'; 3import { test, moduleForComponent } from 'ember-qunit'; 4 5moduleForComponent('single-user', 'Integration | Component | single-user (manual setup)', { 6 integration: true, 7 8 beforeEach: function () { 9 manualSetup(this); 10 } 11}); 12 13test("shows user information", function () { 14 let user = make('user', {name: 'Rob'}); 15 16 this.render(hbs`{{single-user user=user}}`); 17 this.set('user', user); 18 19 ok(this.$('.name').text().match(user.get('name'))); 20 ok(this.$('.funny-name').text().match(user.get('funnyName'))); 21});
- Using
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
- mockFindRecord
- 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:
1 let mock = mockFindAll('user').returns({headers: {'X-Man': "Wolverine"}); 2 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
-
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
- Only used if the mock was set to fail with
-
Use property
timesCalled
to verify how many times the ajax call was mocked- works when you are using
mockQuery
,mockQueryRecord
,mockFindAll
,mockReload
, ormockUpdate
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:
1 const mock = mockQueryRecord('company', {}).returns({ json: build('company') }); 2 3 FactoryGuy.store.queryRecord('company', {}).then(()=> { 4 FactoryGuy.store.queryRecord('company', {}).then(()=> { 5 mock.timesCalled //=> 2 6 }); 7 });
- works when you are using
-
Use method
disable()
to temporarily disable the mock. You can re-enable the disabled mock usingenable()
. -
Use method
destroy()
to completely remove the mock handler for the mock. TheisDestroyed
property is set totrue
when the mock is destroyed.
setup
- As of v2.13.15 mockSetup and mockTeardown are no longer needed
- Use FactoryGuy.settings to set:
- logLevel ( 0 - off , 1 - on ) for seeing the FactoryGuy responses
- responseTime ( in millis ) for simulating slower responses
- Example:
1 FactoryGuy.settings({logLevel: 1, responseTime: 1000});
Using fails method
-
Usable on all mocks
-
Use optional object arguments status and response and convertErrors to customize
- status : must be number in the range of 3XX, 4XX, or 5XX ( default is 500 )
- response : must be object with errors key ( default is null )
- convertErrors : set to false and object will be left untouched ( default is true )
- errors must be in particular format for ember-data to accept them
- FactoryGuy allows you to use a simple style:
{errors: {name: "Name too short"}}
- Behind the scenes converts to another format for ember-data to consume
- FactoryGuy allows you to use a simple style:
- errors must be in particular format for ember-data to accept them
-
Examples:
1 let errors401 = {errors: {description: "Unauthorized"}}; 2 let mock = mockFindAll('user').fails({status: 401, response: errors401}); 3 4 let errors422 = {errors: {name: "Name too short"}}; 5 let mock = mockFindRecord('profile').fails({status: 422, response: errors422}); 6 7 let errorsMine = {errors: [{detail: "Name too short", title: "I am short"}]}; 8 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
orbuild
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:
1 import { build, make, mockFindRecord } from 'ember-data-factory-guy';
- To return default factory model type ( 'user' in this case )
1 // mockFindRecord automatically returns json for the modelType ( in this case 'user' ) 2 let mock = mockFindRecord('user'); 3 let userId = mock.get('id');
- Using
returns({json})
to return json object
1 let user = build('user', 'whacky', {isDude: true}); 2 let mock = mockFindRecord('user').returns({ json: user }); 3 // user.get('id') => 1 4 // user.get('style') => 'whacky' 5 6 // or to acccomplish the same thing with less code 7 let mock = mockFindRecord('user', 'whacky', {isDude: true}); 8 // mock.get('id') => 1 9 // mock.get('style') => 'whacky' 10 let user = mock.get(); 11 // user.id => 1 12 // user.style => 'whacky'
- Using
returns({model})
to return model instance
1 let user = make('user', 'whacky', {isDude: false}); 2 let mock = mockFindRecord('user').returns({ model: user }); 3 // user.get('id') => 1 4 // you can now also user.get('any-computed-property') 5 // since you have a real model instance
- Simper way to return a model instance
1 let user = make('user', 'whacky', {isDude: false}); 2 let mock = mockFindRecord(user); 3 // user.get('id') === mock.get('id') 4 // basically a shortcut to the above .returns({ model: user }) 5 // as this sets up the returns for you
- To reuse the mock
1 let user2 = build('user', {style: "boring"}); 2 mock.returns({ json: user2 }); 3 // mock.get('id') => 2
- To mock failure case use
fails
method
1 mockFindRecord('user').fails();
- To mock failure when you have a model already
1 let profile = make('profile'); 2 mockFindRecord(profile).fails(); 3 // mock.get('id') === profile.id
- To use adapterOptions
1 let mock = mockFindRecord('user').adapterOptions({friendly: true}); 2 // used when urlForFindRecord (defined in adapter) uses them 3 urlForFindRecord(id, modelName, snapshot) { 4 if (snapshot && snapshot.adapterOptions) { 5 let { adapterOptions } = snapshot; // => {friendly: true} 6 // ... blah blah blah 7 } 8 // ... blah blah 9 }
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:
1 import { buildList, makeList, mockFindAll } from 'ember-data-factory-guy';
- To mock and return no results
1 let mock = mockFindAll('user');
- Using
returns({json})
to return json object
1 // that has 2 different users: 2 let users = buildList('user', 'whacky', 'silly'); 3 let mock = mockFindAll('user').returns({ json: users }); 4 let user1 = users.get(0); 5 let user2 = users.get(1); 6 // user1.style => 'whacky' 7 // user2.style => 'silly' 8 9 // or to acccomplish the same thing with less code 10 let mock = mockFindAll('user', 'whacky', 'silly'); 11 let user1 = mock.get(0); 12 let user2 = mock.get(1); 13 // user1.style => 'whacky' 14 // user2.style => 'silly'
- Using
returns({models})
to return model instances
1 let users = makeList('user', 'whacky', 'silly'); 2 let mock = mockFindAll('user').returns({ models: users }); 3 let user1 = users[0]; 4 // you can now also user1.get('any-computed-property') 5 // since you have a real model instance
- To reuse the mock and return different payload
1 let users2 = buildList('user', 3); 2 mock.returns({ json: user2 });
- To mock failure case use
fails()
method
1 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
1 let profile = make('profile'); 2 mockReload(profile); 3 4 // will stub a call to reload that profile 5 profile.reload()
- Using
returns({attrs})
to return new attributes
1 let profile = make('profile', { description: "whatever" }); 2 mockReload(profile).returns({ attrs: { description: "moo" } }); 3 profile.reload(); // description is now "moo"
- Using
returns({json})
to return all new attributes
1 let profile = make('profile', { description: "tomatoes" }); 2 // all new values EXCEPT the profile id ( you should keep that id the same ) 3 let profileAllNew = build('profile', { id: profile.get('id'), description: "potatoes" } 4 mockReload(profile).returns({ json: profileAllNew }); 5 profile.reload(); // description = "potatoes"
- Mocking a failed reload
1 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 method
- Takes modifier methods for matching the query params
-
withParams( object )
-withSomeParams( object )
- Sample acceptance tests using
mockQuery
: user-search-test.js
Usage:
1 import FactoryGuy, { make, build, buildList, mockQuery } from 'ember-data-factory-guy'; 2 let store = FactoryGuy.store; 3 4 // This simulates a query that returns no results 5 mockQuery('user', {age: 10}); 6 7 store.query('user', {age: 10}}).then((userInstances) => { 8 /// userInstances will be empty 9 })
- with returns( models )
1 // Create model instances 2 let users = makeList('user', 2, 'with_hats'); 3 4 mockQuery('user', {name:'Bob', age: 10}).returns({models: users}); 5 6 store.query('user', {name:'Bob', age: 10}}).then((models)=> { 7 // models are the same as the users array 8 });
- with returns ( json )
1 // Create json with buildList 2 let users = buildList('user', 2, 'with_hats'); 3 4 mockQuery('user', {name:'Bob', age: 10}).returns({json: users}); 5 6 store.query('user', {name:'Bob', age: 10}}).then((models)=> { 7 // these models were created from the users json 8 });
- with returns( ids )
1 // Create list of models 2 let users = buildList('user', 2, 'with_hats'); 3 let user1 = users.get(0); 4 5 mockQuery('user', {name:'Bob', age: 10}).returns({ids: [user1.id]}); 6 7 store.query('user', {name:'Bob', age: 10}}).then(function(models) { 8 // models will be one model and it will be user1 9 }); 10
- withParams() / withSomeParams()
1 // Create list of models 2 let users = buildList('user', 2, 'with_hats'); 3 let user1 = users.get(0); 4 5 mock = mockQuery('user').returns({ids: [user1.id]}); 6 7 mock.withParams({name:'Bob', age: 10}) 8 9 // When using 'withParams' modifier, params hash must match exactly 10 store.query('user', {name:'Bob', age: 10}}).then(function(models) { 11 // models will be one model and it will be user1 12 }); 13 14 // The following call will not be caught by the mock 15 store.query('user', {name:'Bob', age: 10, hair: 'brown'}}) 16 17 // 'withSomeParams' is designed to catch requests by partial match 18 // It has precedence over strict params matching once applied 19 mock.withSomeParams({name:'Bob'}) 20 21 // Now both requests will be intercepted 22 store.query('user', {name:'Bob', age: 10}}) 23 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 method
- takes modifier methods for matching the query params - withParams( object )
Usage:
1 import FactoryGuy, { make, build, mockQueryRecord } from 'ember-data-factory-guy'; 2 let store = FactoryGuy.store; 3 4 // This simulates a query that returns no results 5 mockQueryRecord('user', {age: 10}); 6 7 store.queryRecord('user', {age: 10}}).then((userInstance) => { 8 /// userInstance will be empty 9 })
- with returns( models )
1 // Create model instances 2 let user = make('user'); 3 4 mockQueryRecord('user', {name:'Bob', age: 10}).returns({model: user}); 5 6 store.queryRecord('user', {name:'Bob', age: 10}}).then((model)=> { 7 // model is the same as the user you made 8 });
- with returns( json )
1 // Create json with buildList 2 let user = build('user'); 3 4 mockQueryRecord('user', {name:'Bob', age: 10}).returns({json: user}); 5 6 store.queryRecord('user', {name:'Bob', age: 10}}).then((model)=> { 7 // user model created from the user json 8 });
- with returns( ids )
1 // Create list of models 2 let user = build('user', 'with_hats'); 3 4 mockQueryRecord('user', {name:'Bob', age: 10}).returns({id: user.get('id')}); 5 6 store.queryRecord('user', {name:'Bob', age: 10}}).then(function(model) { 7 // model will be one model and it will be user1 8 }); 9
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
- match: takes a hash with attributes or a matching function
- Need to import
run
from@ember/runloop
and wrap tests usingmockCreate
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.
1 2 // most actions that create a record look something like this: 3 action: { 4 addProject: function (user) { 5 let name = this.$('button.project-name').val(); 6 this.store.createRecord('project', {name: name, user: user}).save(); 7 } 8 } 9
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:
1 import { makeNew, mockCreate } from 'ember-data-factory-guy'; 2 3 // Simplest case 4 // Don't care about a match just handle createRecord for any project 5 mockCreate('project'); 6 7 // use a model you created already from store.createRecord or makeNew 8 // need to use this style if you need the model in the urlForCreateRecord snapshot 9 let project = makeNew('project'); 10 mockCreate(project); 11 12 // Matching some attributes 13 mockCreate('project').match({name: "Moo"}); 14 15 // Match all attributes 16 mockCreate('project').match({name: "Moo", user: user}); 17 18 // Match using a function that checks that the request's top level attribute "name" equals 'Moo' 19 mockCreate('project').match(requestData => requestData.name === 'Moo'); 20 21 // Exactly matching attributes, and returning extra attributes 22 mockCreate('project') 23 .match({name: "Moo", user: user}) 24 .returns({created_at: new Date()}); 25 26 // Returning belongsTo relationship. Assume outfit belongsTo 'person' 27 let person = build('super-hero'); // it's polymorphic 28 mockCreate('outfit').returns({attrs: { person }}); 29 30 // Returning hasMany relationship. Assume super-hero hasMany 'outfits' 31 let outfits = buildList('outfit', 2); 32 mockCreate('super-hero').returns({attrs: { outfits }}); 33
- mocking a failed create
1 2 // Mocking failure case is easy with chainable methods, just use #fails 3 mockCreate('project').match({name: "Moo"}).fails(); 4 5 // Can optionally add a status code and/or errors to the response 6 mockCreate('project').fails({status: 422, response: {errors: {name: ['Moo bad, Bahh better']}}}); 7 8 store.createRecord('project', {name: "Moo"}).save(); //=> fails
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 usingmockUpdate
with:run(function() { 'your test' })
Usage:
1 import { make, mockUpdate } from 'ember-data-factory-guy'; 2 3 let profile = make('profile'); 4 5 // Pass in the model that will be updated ( if you have it available ) 6 mockUpdate(profile); 7 8 // If the model is not available, pass in the modelType and the id of 9 // the model that will be updated 10 mockUpdate('profile', 1); 11 12 profile.set('description', 'good value'); 13 profile.save() //=> will succeed 14 15 // Returning belongsTo relationship. Assume outfit belongsTo 'person' 16 let outfit = make('outfit'); 17 let person = build('super-hero'); // it's polymorphic 18 outfit.set('name','outrageous'); 19 mockUpdate(outfit).returns({attrs: { person }}); 20 outfit.save(); //=> saves and returns superhero 21 22 // Returning hasMany relationship. Assume super-hero hasMany 'outfits' 23 let superHero = make('super-hero'); 24 let outfits = buildList('outfit', 2, {name:'bell bottoms'}); 25 superHero.set('style','laid back'); 26 mockUpdate(superHero).returns({attrs: { outfits }}); 27 superHero.save(); // => saves and returns outfits 28 29 // using match() method to specify attribute values 30 let profile = make('profile'); 31 profile.set('name', "woo"); 32 let mock = mockUpdate(profile).match({name: "moo"}); 33 profile.save(); // will not be mocked since the mock you set says the name must be "woo" 34 35 // using match() method to specify a matching function 36 let profile = make('profile'); 37 profile.set('name', "woo"); 38 let mock = mockUpdate(profile).match((requestBody) => { 39 // this example uses a JSONAPI Adapter 40 return requestBody.data.attributes.name === "moo" 41 }); 42 profile.save(); // will not be mocked since the mock you set requires the request's top level attribute "name" to equal "moo" 43 44 // either set the name to "moo" which will now be mocked correctly 45 profile.set('name', "moo"); 46 profile.save(); // succeeds 47 48 // or 49 50 // keep the profile name as "woo" 51 // but change the mock to match the name "woo" 52 mock.match({name: "woo"}); 53 profile.save(); // succeeds
- mocking a failed update
1 let profile = make('profile'); 2 3 // set the succeed flag to 'false' 4 mockUpdate('profile', profile.id).fails({status: 422, response: 'Invalid data'}); 5 // or 6 mockUpdate(profile).fails({status: 422, response: 'Invalid data'}); 7 8 profile.set('description', 'bad value'); 9 profile.save() //=> will fail
mocking a failed update and retry with success
1 let profile = make('profile'); 2 3 let mockUpdate = mockUpdate(profile); 4 5 mockUpdate.fails({status: 422, response: 'Invalid data'}); 6 7 profile.set('description', 'bad value'); 8 profile.save() //=> will fail 9 10 // After setting valid value 11 profile.set('description', 'good value'); 12 13 // Now expecting success 14 mockUpdate.succeeds(); 15 16 // Try that update again 17 profile.save() //=> will succeed!
mockDelete
- Need to import
run
from@ember/runloop
and wrap tests usingmockDelete
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
1 import { make, mockDelete } from 'ember-data-factory-guy'; 2 3 let profile = make('profile'); 4 mockDelete(profile); 5 6 profile.destroyRecord() // => will succeed
- Passing in a model typeName and id
1 import { make, mockDelete } from 'ember-data-factory-guy'; 2 3 let profile = make('profile'); 4 mockDelete('profile', profile.id); 5 6 profile.destroyRecord() // => will succeed
- Passing in a model typeName
1 import { make, mockDelete } from 'ember-data-factory-guy'; 2 3 let profile1 = make('profile'); 4 let profile2 = make('profile'); 5 mockDelete('profile'); 6 7 profile1.destroyRecord() // => will succeed 8 profile2.destroyRecord() // => will succeed
- Mocking a failed delete
1 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 toGET
- 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
- type: The HTTP verb (
Usage:
- Simple case
1 import { mock } from 'ember-data-factory-guy'; 2 3 this.mock({ url: '/users' });
- Returning a JavaScript object
1 import { mock } from 'ember-data-factory-guy'; 2 3 this.mock({ 4 type: 'POST', 5 url: '/users/sign_in', 6 responseText: { token: "0123456789-ab" } 7 }); 8
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:
1import { getPretender } from 'ember-data-factory-guy'; 2 3// Passthrough 'data:' requests. 4getPretender().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.
1 let json = buildList('widget', 'square', 'round', ['round','broken']); 2 let widgets = makeList('widget', 'square', 'round', ['round','broken']); 3 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:
1 import FactoryGuy from 'ember-data-factory-guy'; 2 3 FactoryGuy.define('state', { 4 5 traits: { 6 NY: { name: "New York", id: "NY" }, 7 NJ: { name: "New Jersey", id: "NJ" }, 8 CT: { name: "Connecticut", id: "CT" } 9 } 10 }); 11 12 // then in your tests you would do 13 let [ny, nj, ct] = makeList('state', 'ny', 'nj', 'ct');
- Or you could use a strategy to get them like this:
1 import FactoryGuy from 'ember-data-factory-guy'; 2 3 const states = [ 4 { name: "New York", id: "NY" }, 5 { name: "New Jersey", id: "NJ" }, 6 { name: "Connecticut", id: "CT" } 7 ... blah .. blah .. blah 8 ]; 9 10 FactoryGuy.define('state', { 11 12 default: { 13 id: FactoryGuy.generate((i)=> states[i-1].id), 14 name: FactoryGuy.generate((i)=> states[i-1].name) 15 } 16 }); 17 18 // then in your tests you would do 19 let states = makeList('state', 3); // or however many states you have
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:
1// file: tests/scenarios/admin.js 2import Ember from 'ember'; 3import {Scenario} from 'ember-data-factory-guy'; 4 5export default class extends Scenario { 6 7 run() { 8 this.createGroups(); 9 } 10 11 createGroups() { 12 this.permissionGroups = this.makeList('permission-group', 3); 13 } 14 15 groupNames() { 16 return this.permissionGroups.mapBy('name').sort(); 17 } 18} 19 20// file: tests/acceptance/admin-view-test.js 21import page from '../pages/admin'; 22import Scenario from '../scenarios/admin'; 23 24describe('Admin View', function() { 25 let scenario; 26 27 beforeEach(function() { 28 scenario = new Scenario(); 29 scenario.run(); 30 }); 31 32 describe('group', function() { 33 beforeEach(function() { 34 page.visitGroups(); 35 }); 36 37 it('shows all groups', function() { 38 expect(page.groups.names).to.arrayEqual(scenario.groupNames()); 39 }); 40 }); 41});
Tip 4: Testing mocks ( async testing ) in unit tests
- Two ways to handle asyncronous test
- async / await ( most elegant ) Sample test
- need to declare polyfill for ember-cli-babel options in ember-cli-build
- using
assert.async()
(qunit) /done
(mocha) Sample test
- async / await ( most elegant ) Sample test
Tip 5: Testing model's custom serialize()
method
- The fact that you can match on attributes in
mockUpdate
andmockCreate
means that you can test a customserialize()
method in a model serializer
1 2 // app/serializers/person.js 3 export default class PersonSerializer extends RESTSerializer { 4 5 // let's say you're modifying all names to be Japanese honorific style 6 serialize(snapshot, options) { 7 var json = this._super(snapshot, options); 8 9 let honorificName = [snapshot.record.get('name'), 'san'].join('-'); 10 json.name = honorificName; 11 12 return json; 13 } 14 } 15 16 // somewhere in your tests 17 let person = make('person', {name: "Daniel"}); 18 mockUpdate(person).match({name: "Daniel-san"}); 19 person.save(); // will succeed 20 // and voila, you have just tested the serializer is converting the name properly
- You could also test
serialize()
method in a simpler way by doing this:
1 let person = make('person', {name: "Daniel"}); 2 let json = person.serialize(); 3 assert.equal(json.name, 'Daniel-san');
ChangeLog
No vulnerabilities found.
Reason
12 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Reason
no dangerous workflow patterns detected
Reason
no binaries found in the repo
Reason
license file detected
Details
- Info: project has a license file: LICENSE.md:0
- Info: FSF or OSI recognized license: MIT License: LICENSE.md:0
Reason
packaging workflow detected
Details
- Info: Project packages its releases by way of GitHub Actions.: .github/workflows/publish.yml:36
Reason
Found 5/19 approved changesets -- score normalized to 2
Reason
detected GitHub workflow tokens with excessive permissions
Details
- Warn: jobLevel 'contents' permission set to 'write': .github/workflows/plan-release.yml:39
- Info: jobLevel 'issues' permission set to 'read': .github/workflows/plan-release.yml:40
- Warn: jobLevel 'contents' permission set to 'write': .github/workflows/publish.yml:42
- Warn: no topLevel permission defined: .github/workflows/ci.yml:1
- Warn: no topLevel permission defined: .github/workflows/plan-release.yml:1
- Warn: no topLevel permission defined: .github/workflows/publish.yml:1
Reason
dependency not pinned by hash detected -- score normalized to 0
Details
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:45: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/ci.yml/master?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci.yml:46: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/ci.yml/master?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:47: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/ci.yml/master?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:82: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/ci.yml/master?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci.yml:83: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/ci.yml/master?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:84: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/ci.yml/master?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci.yml:96: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/ci.yml/master?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:110: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/ci.yml/master?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci.yml:111: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/ci.yml/master?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:112: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/ci.yml/master?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/ci.yml/master?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci.yml:23: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/ci.yml/master?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:24: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/ci.yml/master?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/plan-release.yml:24: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/plan-release.yml/master?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/plan-release.yml:49: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/plan-release.yml/master?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/plan-release.yml:55: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/plan-release.yml/master?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/plan-release.yml:58: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/plan-release.yml/master?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/plan-release.yml:79: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/plan-release.yml/master?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:26: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/publish.yml/master?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:46: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/publish.yml/master?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/publish.yml:47: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/publish.yml/master?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/publish.yml:52: update your workflow using https://app.stepsecurity.io/secureworkflow/adopted-ember-addons/ember-data-factory-guy/publish.yml/master?enable=pin
- Info: 0 out of 14 GitHub-owned GitHubAction dependencies pinned
- Info: 0 out of 8 third-party GitHubAction dependencies pinned
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
project is not fuzzed
Details
- Warn: no fuzzer integrations found
Reason
branch protection not enabled on development/release branches
Details
- Warn: branch protection not enabled for branch 'master'
Reason
security policy file not detected
Details
- Warn: no security policy file detected
- Warn: no security file to analyze
- Warn: no security file to analyze
- Warn: no security file to analyze
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
- Warn: 0 commits out of 23 are checked with a SAST tool
Reason
11 existing vulnerabilities detected
Details
- Warn: Project is vulnerable to: GHSA-whgm-jr23-g3j9
- Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92
- Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg
- Warn: Project is vulnerable to: GHSA-wxhq-pm8v-cw75
- Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275
- Warn: Project is vulnerable to: GHSA-pfrx-2q88-qq97
- Warn: Project is vulnerable to: GHSA-9c47-m6qq-7p4h
- Warn: Project is vulnerable to: GHSA-35jh-r3h4-6jhm
- Warn: Project is vulnerable to: GHSA-6vfc-qv3f-vr6c
- Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv
- Warn: Project is vulnerable to: GHSA-gcx4-mw62-g8wm
Score
4
/10
Last Scanned on 2024-11-25
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn MoreOther packages similar to ember-data-factory-guy
@eflexsystems/ember-data-factory-guy
Factories for testing Ember applications using EmberData
ember-factory-for-polyfill
Provides a basice polyfill for the ember-factory-for feature.
micromark-factory-destination
micromark factory to parse destinations (found in resources, definitions)
micromark-factory-space
micromark factory to parse markdown space (found in lots of places)