UNPKG

ember-data-factory-guy

Version:
1,421 lines (1,102 loc) 71.9 kB
# Ember Data Factory Guy [![Build Status](https://github.com/adopted-ember-addons/ember-data-factory-guy/workflows/ci/badge.svg)](https://github.com/adopted-ember-addons/ember-data-factory-guy/actions/workflows/ci.yml) [![Ember Observer Score](http://emberobserver.com/badges/ember-data-factory-guy.svg)](http://emberobserver.com/addons/ember-data-factory-guy) [![npm version](https://badge.fury.io/js/ember-data-factory-guy.svg)](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