ember-data-factory-guy
Version:
Factories for testing Ember applications using EmberData
1,421 lines (1,102 loc) • 71.9 kB
Markdown
# Ember Data Factory Guy
[](https://github.com/adopted-ember-addons/ember-data-factory-guy/actions/workflows/ci.yml) [](http://emberobserver.com/addons/ember-data-factory-guy) [](http://badge.fury.io/js/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](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/dummy/app/adapters/application.js)
**NEW** starting with v3.2.1
- You can setup data AND links for your async relationship [Check it out](#special-tips-for-links)
**NEW** You can use factory guy in ember-twiddle
- Using [Scenarios](https://ember-twiddle.com/421f16ecc55b5d35783c243b8d99f2be?openFiles=tests.unit.model.user-test.js%2C)
**NEW** If using new style of ember-qunit acceptance tests with ```setupApplicationTest``` check out demo here: [user-view-test.js:](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/acceptance/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](#tip-6-using-traits-as-functions)
**Older but still fun things**
- Support for **[ember-data-model-fragment](https://github.com/lytics/ember-data-model-fragments)** usage is baked in since v2.5.0
- Support for **[ember-django-adapter](https://github.com/dustinfarris/ember-django-adapter)** usage is fried in since v2.6.1
- Support for adding [meta data](#using-add-method) 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](https://embercommunity.slack.com/messages/e-factory-guy/) Slack channel
### Contents
- [How It Works](#how-it-works)
- [Installation](#installation)
- [Upgrading](#upgrading)
- [Setup](#setup)
- [Defining Factories](#defining-factories)
- [Using Factories](#using-factories)
- [Using in Development, Production or other environments](#using-in-other-environments)
- [Ember Data Model Fragments](#ember-data-model-fragments)
- [Creating Factories in Addons](#creating-factories-in-addons)
- [Ember Django Adapter](#ember-django-adapter)
- [Custom API formats](#custom-api-formats)
- [Testing models, controllers, components](#testing-models-controllers-components)
- [Acceptance Tests](#acceptance-tests)
- [Pretender](#pretender)
- [Tips and Tricks](#tips-and-tricks)
- [Changelog](#changelog)
### 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:
```javascript
// standard models
class User extends Model {
@attr('string') name
@attr('string') style
@hasMany('project') projects
@hasMany('hat', {polymorphic: true}) hats
}
class Project extends Model {
@attr('string') title
@belongsTo('user') user
}
// polymorphic models
class Hat extends Model {
@attr('string') type
@belongsTo('user') user
}
class BigHat extends Hat {};
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 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
- Sample full blown factory: [`user.js`](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/dummy/app/tests/factories/user.js)
- Brief sample of a factory definition:
```javascript
// file tests/factories/user.js
import FactoryGuy from 'ember-data-factory-guy';
FactoryGuy.define('user', {
// Put default 'user' attributes in the default section
default: {
style: 'normal',
name: 'Dude'
},
// Create a named 'user' with custom attributes
admin: {
style: 'super',
name: 'Admin'
}
});
```
- If you are using an attribute named `type` and this is not a polymorphic model, use the option
```polymorphic: false``` in your definition
```javascript
// file: tests/factories/cat.js
FactoryGuy.define('cat', {
polymorphic: false, // manually flag this model as NOT polymorphic
default: {
// usually, an attribute named 'type' is for polymorphic models, but the defenition
// is set as NOT polymorphic, which allows this type to work as attibute
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](iel/ember-data-factory-guy#extending-other-definitions))
```javascript
// file tests/factories/small-hat.js
import FactoryGuy from 'ember-data-factory-guy';
FactoryGuy.define('small-hat', {
default: {
type: 'SmallHat'
}
})
// file tests/factories/big-hat.js
import FactoryGuy from 'ember-data-factory-guy';
FactoryGuy.define('big-hat', {
default: {
type: 'BigHat'
}
})
```
In other words, don't do this:
```javascript
// file tests/factories/hat.js
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
```javascript
FactoryGuy.define('user', {
sequences: {
userName: (num)=> `User${num}`
},
default: {
// use the 'userName' sequence for this attribute
name: FactoryGuy.generate('userName')
}
});
let first = FactoryGuy.build('user');
first.get('name') // => 'User1'
let second = FactoryGuy.make('user');
second.get('name') // => 'User2'
```
##### Declaring an inline sequence on attribute
```javascript
FactoryGuy.define('project', {
special_project: {
title: FactoryGuy.generate((num)=> `Project #${num}`)
},
});
let json = FactoryGuy.build('special_project');
json.get('title') // => 'Project #1'
let project = FactoryGuy.make('special_project');
project.get('title') // => 'Project #2'
```
### Inline Functions
- Declare a function for an attribute
- The fixture is passed as parameter so you can reference
all other attributes, even id
```javascript
FactoryGuy.define('user', {
default: {
// Don't need the userName sequence, since the id is almost
// always a sequential number, and you can use that.
// f is the fixture being built as the moment for this factory
// definition, which has the id available
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') // => 'User1'
json.get('style') // => 'funny User1'
let user = FactoryGuy.make('user', 'boring');
user.get('id') // => 2
user.get('style') // => '2 boring'
```
*Note the style attribute was built from a function which depends on the name
and the name is a generated attribute from a sequence function*
### 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 )
```javascript
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') // => 'Big Guy'
user.get('style') // => 'Friendly'
let giant = FactoryGuy.make('user', 'big', 'bfg');
user.get('name') // => 'Big Friendly Giant' - name defined in the 'bfg' trait overrides the name defined in the 'big' trait
user.get('style') // => 'Friendly'
```
You can still pass in a hash of options when using traits. This hash of
attributes will override any trait attributes or default attributes
```javascript
let user = FactoryGuy.make('user', 'big', 'friendly', {name: 'Dave'});
user.get('name') // => 'Dave'
user.get('style') // => 'Friendly'
```
##### Using traits as functions
```js
import FactoryGuy from 'ember-data-factory-guy';
FactoryGuy.define("project", {
default: {
title: (f) => `Project ${f.id}`
},
traits: {
// this trait is a function
// note that the fixure is passed in that will have
// default attributes like id at a minimum and in this
// case also a title ( `Project 1` ) which is default
medium: (f) => {
f.title = `Medium Project ${f.id}`
},
goofy: (f) => {
f.title = `Goofy ${f.title}`
}
withUser: (f) => {
// NOTE: you're not using FactoryGuy.belongsTo as you would
// normally in a fixture definition
f.user = FactoryGuy.make('user')
}
}
});
```
So, when you make / build a project like:
```js
let project = make('project', 'medium');
project.get('title'); //=> 'Medium Project 1'
let project2 = build('project', 'goofy');
project2.get('title'); //=> 'Goofy Project 2'
let project3 = build('project', 'withUser');
project3.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` and `FactoryGuy.make`/`FactoryGuy.makeList`
- Can compose relationships to any level
- When setting up manually do not mix `build` and `make` - you either `build` JSON in every levels of associations or `make` objects. `build` is taking serializer into account for every model which means that output from `build` might be different than expected input defined in factory in `make`.
- Special tips for links
##### Setup belongsTo associations in Factory Definitions
- using traits are the best practice
```javascript
FactoryGuy.define('project', {
traits: {
withUser: { user: {} },
withAdmin: { user: FactoryGuy.belongsTo('user', 'admin') },
withManagerLink(f) { // f is the fixture being created
f.links = {manager: `/projects/${f.id}/manager`}
}
}
});
let user = make('project', 'withUser');
project.get('user').toJSON({includeId: true}) // => {id:1, name: 'Dude', style: 'normal'}
user = make('user', 'withManagerLink');
user.belongsTo('manager').link(); // => "/projects/1/manager"
```
##### Setup belongsTo associations manually
See `FactoryGuy.build`/`FactoryGuy.buildList` for more ideas
```javascript
let user = make('user');
let project = make('project', {user});
project.get('user').toJSON({includeId: true}) // => {id:1, name: 'Dude', style: 'normal'}
```
*Note that though you are setting the 'user' belongsTo association on a project,
the reverse user hasMany 'projects' association is being setup for you on the user
( for both manual and factory defined belongsTo associations ) as well*
```javascript
user.get('projects.length') // => 1
```
##### 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.
```javascript
FactoryGuy.define('user', {
traits: {
withProjects: {
projects: FactoryGuy.hasMany('project', 2)
},
withPropertiesLink(f) { // f is the fixture being created
f.links = {properties: `/users/${f.id}/properties`}
}
}
});
let user = make('user', 'withProjects');
user.get('projects.length') // => 2
user = make('user', 'withPropertiesLink');
user.hasMany('properties').link(); // => "/users/1/properties"
```
*You could also setup a custom named user definition:*
```javascript
FactoryGuy.define('user', {
userWithProjects: { projects: FactoryGuy.hasMany('project', 2) }
});
let user = make('userWithProjects');
user.get('projects.length') // => 2
```
##### Setup hasMany associations manually
See `FactoryGuy.build`/`FactoryGuy.makeList` for more ideas
```javascript
let project1 = make('project');
let project2 = make('project');
let user = make('user', {projects: [project1, project2]});
user.get('projects.length') // => 2
// or
let projects = makeList('project', 2);
let user = make('user', {projects});
user.get('projects.length') // => 2
```
*Note that though you are setting the 'projects' hasMany association on a user,
the reverse 'user' belongsTo association is being setup for you on the project
( for both manual and factory defined hasMany associations ) as well*
```javascript
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
```javascript
FactoryGuy.define('user', {
traits: {
withCompanyLink(f): {
// since you can assign many different links with different traits,
// you should Object.assign so that you add to the links hash rather
// than set it directly ( assuming you want to use this feature )
f.links = Object.assign({company: `/users/${f.id}/company`}, f.links);
},
withPropertiesLink(f) {
f.links = Object.assign({properties: `/users/${f.id}/properties`}, f.links);
}
}
});
// setting links with traits
let company = make('company')
let user = make('user', 'withCompanyLink', 'withPropertiesLink', {company});
user.hasMany('properties').link(); // => "/users/1/properties"
user.belongsTo('company').link(); // => "/users/1/company"
// the company async relationship has a company AND link to fetch it again
// when you reload that relationship
user.get('company.content') // => company
user.belongsTo('company').reload() // would use that link "/users/1/company" to reload company
// you can also set traits with your build/buildList/make/makeList options
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`](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/dummy/app/tests/factories/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:
```javascript
// app/models/dog.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default class Dog extends Model{
@attr('string') dogNumber
@attr('string') sound
}
// tests/factories/dog.js
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:
```javascript
let dog2 = build('dog', { volume: 'Soft' });
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
time `onload` is called.
- In this case, just use transient attributes without the `afterMake`
Assuming the factory-guy model definition defines `afterMake` function:
```javascript
FactoryGuy.define('property', {
default: {
name: 'Silly property'
},
// optionally set transient attributes, that will be passed in to afterMake function
transient: {
for_sale: true
},
// The attributes passed to after make will include any optional attributes you
// passed in to make, and the transient attributes defined in this definition
afterMake: function(model, attributes) {
if (attributes.for_sale) {
model.set('name', model.get('name') + '(FOR SALE)');
}
}
}
```
You would use this to make models like:
```javascript
run(function () {
let property = FactoryGuy.make('property');
property.get('name'); // => 'Silly property(FOR SALE)')
let property = FactoryGuy.make('property', {for_sale: false});
property.get('name'); // => 'Silly property')
});
```
Remember to import the `run` function with `import { run } from "@ember/runloop"`;
### Using Factories
- [`FactoryGuy.attributesFor`](#factoryguyattributesfor)
- returns attributes ( for now no relationship info )
- [`FactoryGuy.make`](#factoryguymake)
- push model instances into store
- [`FactoryGuy.makeNew`](#factoryguymakenew)
- Create a new model instance but doesn't load it to the store
- [`FactoryGuy.makeList`](#factoryguymakelist)
- Loads zero to many model instances into the store
- [`FactoryGuy.build`](#factoryguybuild)
- Builds json in accordance with the adapter's specifications
- [RESTAdapter](http://emberjs.com/api/data/classes/DS.RESTAdapter.html) (*assume this adapter being used in most of the following examples*)
- [ActiveModelAdapter](https://github.com/ember-data/active-model-adapter#json-structure)
- [JSONAPIAdapter](http://jsonapi.org/format/)
- [DrfAdapter (Ember Django Adapter)](https://github.com/dustinfarris/ember-django-adapter)
- [`FactoryGuy.buildList`](#factoryguybuildlist)
- 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](#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 )
```javascript
import { attributesFor } from 'ember-data-factory-guy';
// make a user with certain traits and options
attributesFor('user', 'silly', {name: 'Fred'}); // => { name: 'Fred', style: 'silly'}
```
##### `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
```javascript
import { make } from 'ember-data-factory-guy';
// make a user with the default attributes in user factory
let user = make('user');
user.toJSON({includeId: true}); // => {id: 1, name: 'User1', style: 'normal'}
// make a user with the default attributes plus those defined as 'admin' in the user factory
let user = make('admin');
user.toJSON({includeId: true}); // => {id: 2, name: 'Admin', style: 'super'}
// make a user with the default attributes plus these extra attributes provided in the optional hash
let user = make('user', {name: 'Fred'});
user.toJSON({includeId: true}); // => {id: 3, name: 'Fred', style: 'normal'}
// make an 'admin' user with these extra attributes
let user = make('admin', {name: 'Fred'});
user.toJSON({includeId: true}); // => {id: 4, name: 'Fred', style: 'super'}
// make a user with a trait ('silly') plus these extra attributes provided in the optional hash
let user = make('user', 'silly', {name: 'Fred'});
user.toJSON({includeId: true}); // => {id: 5, name: 'Fred', style: 'silly'}
// make a user with a hats relationship ( hasMany ) composed of pre-made hats
let hat1 = make('big-hat');
let hat2 = make('big-hat');
let user = make('user', {hats: [hat1, hat2]});
user.toJSON({includeId: true})
// => {id: 6, name: 'User2', style: 'normal', hats: [{id:1, type:"big_hat"},{id:1, type:"big_hat"}]}
// note that hats are polymorphic. if they weren't, the hats array would be a list of ids: [1,2]
// make a user with a company relationship ( belongsTo ) composed of a pre-made company
let company = make('company');
let user = make('user', {company: company});
user.toJSON({includeId: true}) // => {id: 7, name: 'User3', style: 'normal', company: 1}
// make user with links to async hasMany properties
let user = make('user', {properties: {links: '/users/1/properties'}});
// make user with links to async belongsTo company
let user = make('user', {company: {links: '/users/1/company'}});
// for model fragments you get an object
let object = make('name'); // => {firstName: 'Boba', lastName: 'Fett'}
```
##### `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):](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/dummy/app/tests/factories/user.js) to see 'bob' user and 'with_car' trait
Usage:
```javascript
import { make, makeList } from 'ember-data-factory-guy';
// Let's say bob is a named type in the user factory
makeList('user', 'bob') // makes 0 bob's
makeList('user', 'bob', 2) // makes 2 bob's
makeList('user', 'bob', 2, 'with_car', {name: "Dude"})
// makes 2 bob users with the 'with_car' trait and name of "Dude"
// In other words, applies the traits and options to every bob made
makeList('user', 'bob', 'with_car', ['with_car', {name: "Dude"}])
// makes 2 users with bob attributes. The first also has the 'with_car' trait and the
// second has the 'with_car' trait and name of "Dude", so you get 2 different users
```
##### `FactoryGuy.build`
- for building json that you can pass as json payload in [acceptance tests](#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`](#using-add-method) 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:
```javascript
import { build, buildList } from 'ember-data-factory-guy';
// build a basic user with the default attributes from the user factory
let json = build('user');
json.get() // => {id: 1, name: 'User1', style: 'normal'}
// build a user with the default attributes plus those defined as 'admin' in the user factory
let json = build('admin');
json.get() // => {id: 2, name: 'Admin', style: 'super'}
// build a user with the default attributes with extra attributes
let json = build('user', {name: 'Fred'});
json.get() // => {id: 3, name: 'Fred', style: 'normal'}
// build the admin defined user with extra attributes
let json = build('admin', {name: 'Fred'});
json.get() // => {id: 4, name: 'Fred', style: 'super'}
// build default user with traits and with extra attributes
let json = build('user', 'silly', {name: 'Fred'});
json.get() // => {id: 5, name: 'Fred', style: 'silly'}
// build user with hats relationship ( hasMany ) composed of a few pre 'built' hats
let hat1 = build('big-hat');
let hat2 = build('big-hat');
let json = build('user', {hats: [hat1, hat2]});
// note that hats are polymorphic. if they weren't, the hats array would be a list of ids: [1,2]
json.get() // => {id: 6, name: 'User2', style: 'normal', hats: [{id:1, type:"big_hat"},{id:1, type:"big_hat"}]}
// build user with company relationship ( belongsTo ) composed of a pre 'built' company
let company = build('company');
let json = build('user', {company});
json.get() // => {id: 7, name: 'User3', style: 'normal', company: 1}
// build and compose relationships to unlimited degree
let company1 = build('company', {name: 'A Corp'});
let company2 = build('company', {name: 'B Corp'});
let owners = buildList('user', { company:company1 }, { company:company2 });
let buildJson = build('property', { owners });
// build user with links to async hasMany properties
let user = build('user', {properties: {links: '/users/1/properties'}});
// build user with links to async belongsTo company
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
```javascript
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](#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`](#using-add-method) method
- to add extra sideloaded data to the payload => `.add(payload)`
- to add meta data => `.add({meta})`
Usage:
```javascript
import { build, buildList } from 'ember-data-factory-guy';
let bobs = buildList('bob', 2); // builds 2 Bob's
let bobs = buildList('bob', 2, {name: 'Rob'}); // builds 2 Bob's with name of 'Rob'
// builds 2 users, one with name 'Bob' , the next with name 'Rob'
let users = buildList('user', { name:'Bob' }, { name:'Rob' });
// builds 2 users, one with 'boblike' and the next with name 'adminlike' features
// NOTE: you don't say how many to make, because each trait is making new user
let users = buildList('user', 'boblike', 'adminlike');
// builds 2 users:
// one 'boblike' with stoner style
// and the next 'adminlike' with square style
// NOTE: how you are grouping traits and attributes for each one by wrapping them in array
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:
```javascript
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:
```javascript
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)=> // first 2 from json1
store.query('profile', {page: 3}).then((records)=> // second 2 from json2
```
##### Using `get()` method
- for inspecting contents of json payload
- `get()` returns all attributes of top level model
- `get(attribute)` gives you an attribute from the top level model
- `get(index)` gives you the info for a hasMany relationship at that index
- `get(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:](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/dummy/app/tests/factories/user.js) to see 'boblike' and 'adminlike' user traits
```javascript
let json = build('user');
json.get() //=> {id: 1, name: 'User1', style: 'normal'}
json.get('id') // => 1
let json = buildList('user', 2);
json.get(0) //=> {id: 1, name: 'User1', style: 'normal'}
json.get(1) //=> {id: 2, name: 'User2', style: 'normal'}
let json = buildList('user', 'boblike', 'adminlike');
json.get(0) //=> {id: 1, name: 'Bob', style: 'boblike'}
json.get(1) //=> {id: 2, name: 'Admin', style: 'super'}
```
* building relationships inline
```javascript
let json = build('user', 'with_company', 'with_hats');
json.get() //=> {id: 1, name: 'User1', style: 'normal'}
// to get hats (hasMany relationship) info
json.get('hats') //=> [{id: 1, type: "big_hat"},{id: 1, type: "big_hat"}]
// to get company ( belongsTo relationship ) info
json.get('company') //=> {id: 1, type: "company"}
```
* by composing the relationships you can get the full attributes of those associations
```javascript
let company = build('company');
let hats = buildList('big-hats');
let user = build('user', {company , hats});
user.get() //=> {id: 1, name: 'User1', style: 'normal'}
// to get hats info from hats json
hats.get(0) //=> {id: 1, type: "BigHat", plus .. any other attributes}
hats.get(1) //=> {id: 2, type: "BigHat", plus .. any other attributes}
// to get company info
company.get() //=> {id: 1, type: "Company", name: "Silly corp"}
```
### 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.
```javascript
// file: config/environment.js
// in development you don't have to set enabled to true since that is default
if (environment === 'development') {
ENV.factoryGuy = { useScenarios: true };
ENV.locationType = 'auto';
ENV.rootURL = '/';
}
// or
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
```javascript
// file: app/scenarios/main.js
import {Scenario} from 'ember-data-factory-guy';
import Users from './users';
// Just for fun, set the log level ( to 1 ) and see all FactoryGuy response info in console
Scenario.settings({
logLevel: 1, // 1 is the max for now, default is 0
});
export default class extends Scenario {
run() {
this.include([Users]); // include other scenarios
this.mockFindAll('products', 3); // mock some finds
this.mock({
type: 'POST',
url: '/api/v1/users/sign_in',
responseText: { token:"0123456789-ab" }
}); // mock a custom endpoint
}
}
```
```javascript
// file: app/scenarios/users.js
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](https://github.com/lytics/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.
```javascript
// Employee model
export default class Employee extends Model {
@fragment('name') name
@fragmentArray('phone-number') phoneNumbers
}
// Name fragment
export default class Name extends Fragment {
@array('string') titles
@attr('string') firstName
@attr('string') lastName
}
// Phone Number fragment
export default class PhoneNumber extends Fragment {
@attr('string') number
@attr('string') type
}
```
A factory for this model and its fragments would look like so:
```javascript
// Employee factory
FactoryGuy.define('employee', {
default: {
name: FactoryGuy.belongsTo('name'), //fragment
phoneNumbers: FactoryGuy.hasMany('phone-number') //fragmentArray
}
});
// Name fragment factory
FactoryGuy.define('name', {
default: {
titles: ['Mr.', 'Dr.'], //array
firstName: 'Jon',
lastName: 'Snow'
}
});
// Phone number fragment factory
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:
```js
let phoneNumbers = makeList('phone-numbers', 2);
let employee = make('employee', { phoneNumbers });
// OR
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:
- model test [employee test](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/unit/models/employee-test.js).
- acceptance test [employee-view-test](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/acceptance/employee-view-test.js).
### 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:
```javascript
let projects = makeList('projects', 2); // put projects in the store
let user = make('user', { projects }); // attach them to user
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()`
### 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:
```javascript
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();
// the store.findRecord call for the user will go out unless there is a user
// in the store
make('user', {name: 'current'});
// the application starts up and makes calls to findAll a few things, but
// those can be ignored because of the cacheOnlyMode
// for this test I care about just testing profiles
makeList("profile", 2);
await visit('/profiles');
// test stuff
});
test("Using FactoryGuy.cacheOnlyMode with except", async function() {
FactoryGuy.cacheOnlyMode({except: ['profile']});
make('user', {name: 'current'});
// this time I want to allow the ajax call so I can return built json payload
mockFindAll("profile", 2);
await visit('/profiles');
// test stuff
});
```
### 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)
```js
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](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/unit/models/profile-test.js)
- Use `moduleForModel` ( ember-qunit ), or `describeModel` ( ember-mocha ) test helper
- manually set up FactoryGuy
- Sample component test: [single-user-test.js](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/components/single-user-test.js)
- Using `moduleForComponent` ( ember-qunit ), or `describeComponent` ( ember-mocha ) helper
- manually sets up FactoryGuy
```javascript
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:](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/acceptance/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](#mockfindrecord)
- [mockFindAll](#mockfindall)
- [mockReload](#mockreload)
- [mockQuery](#mockquery)
- [mockQueryRecord](#mockqueryrecord)
- takes modifier method `returns()` for setting the payload response
- `returns()` accepts parameters like: json, model, models, id, ids, headers
- headers are cumulative so you can add as many as you like
- Example:
```javascript
let mock = mockFindAll('user').returns({headers: {'X-Man': "Wolverine"});
mock.returns({headers: {'X-Weapon': "Claws"}});
- these mocks are are reusable
- so you can simulate making the same ajax call ( url ) and return a different payload
- http POST/PUT/DELETE
- [mockCreate](#mockcreate)
- [mockUpdate](#mockupdate)
- [mockDelete](#mockdelete)
- Custom mocks (http GET/POST/PUT/DELETE)
- [mock](#mock)
- 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:
```javascript
const mock = mockQueryRecord('company', {}).returns({ json: build('company') });
FactoryGuy.store.queryRecord('company', {}).then(()=> {
FactoryGuy.store.queryRecord('company', {}).then(()=> {
mock.timesCalled //=> 2
});
});
```
- 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
- 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:
```javascript
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
- Examples:
```javascript
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`](#factoryguymake) or [`build`](#factoryguybuild)
- `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:](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/acceptance/user-view-test.js)
Usage:
```javascript
import { build, make, mockFindRecord } from 'ember-data-factory-guy';
```
- To return default factory model type ( 'user' in this case )
```javascript
// mockFindRecord automatically returns json for the modelType ( in this case 'user' )
let mock = mockFindRecord('user');
let userId = mock.get('id');
```
- Using `returns({json})` to return json object
```javascript
let user = build('user', 'whacky', {isDude: true});
let mock = mockFindRecord('user').returns({ json: user });
// user.get('id') => 1
// user.get('style') => 'whacky'
// or to acccomplish the same thing with less code
let mock = mockFindRecord('user', 'whacky', {isDude: true});
// mock.get('id') => 1
// mock.get('style') => 'whacky'
let user = mock.get();
// user.id => 1
// user.style => 'whacky'
```
- Using `returns({model})` to return model instance
```javascript
let user = make('user', 'whacky', {isDude: false});
let mock = mockFindRecord('user').returns({ model: user });
// user.get('id') => 1
// you can now also user.get('any-computed-property')
// since you have a real model instance
```
- Simper way to return a model instance
```javascript
let user = make('user', 'whacky', {isDude: false});
let mock = mockFindRecord(user);
// user.get('id') === mock.get('id')
// basically a shortcut to the above .returns({ model: user })
// as this sets up the returns for you
```
- To reuse the mock
```javascript
let user2 = build('user', {style: "boring"});
mock.returns({ json: user2 });
// mock.get('id') => 2
```
- To mock failure case use `fails` method
```javascript
mockFindRecord('user').fails();
```
- To mock failur