From 2776088c9b0c9183ed673c086ae1a594b37a403c Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Sun, 12 Mar 2023 21:21:56 +0100 Subject: [PATCH] finish first draft of udpated doc --- README.md | 2123 +----------------------------------- docs/_sidebar.md | 6 + docs/advanced.md | 393 +++++++ docs/defining-factories.md | 452 ++++++++ docs/index.html | 5 +- docs/overview.md | 16 + docs/quick-start.md | 33 + docs/setup.md | 45 + docs/tips.md | 158 +++ docs/using-factories.md | 462 ++++++++ docs/using-mock-methods.md | 753 +++++++++++++ 11 files changed, 2336 insertions(+), 2110 deletions(-) create mode 100644 docs/advanced.md create mode 100644 docs/defining-factories.md create mode 100644 docs/quick-start.md create mode 100644 docs/setup.md create mode 100644 docs/tips.md create mode 100644 docs/using-factories.md create mode 100644 docs/using-mock-methods.md diff --git a/README.md b/README.md index 4777bddd..79a796ae 100644 --- a/README.md +++ b/README.md @@ -1,2120 +1,31 @@ -# Ember Data Factory Guy +[![Build Status](https://github.com/adopted-ember-addons/ember-data-factory-guy/actions/workflows/ci.yml/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) -[![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) +# Overview 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) +## Features -**NEW** You can use factory guy in ember-twiddle - - Using [Scenarios](https://ember-twiddle.com/421f16ecc55b5d35783c243b8d99f2be?openFiles=tests.unit.model.user-test.js%2C) +- 🚀 Easily push models to the Store, even the most complex relationships +- ⚡️️ Simple, Powerful, & Intuitive API +- ⭐ Works out of the box with your existing serializers and adapters +- 💎 Multiple build strategies +- 🕵️ Intercept and spy on Store requests +- 🕐 Slow down response times -**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) +## Getting Started -**NEW** starting with v2.13.27 - - get attributes for factory defined models with ```attributesFor``` +Check out the [Quick Start](quick-start.md) documentation to get started. -**NEW** starting with v2.13.24 - - manualSetup streamlined to ```manualSetup(this)``` +## Questions / Get in Touch -**NEW and Improved** starting with v2.13.22 - - Traits can be functions [Check it out](#tip-6-using-traits-as-functions) +Visit the EmberJS Community on [Discord](https://discord.com/channels/480462759797063690/483601670685720591). -**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 +## Contributing -**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 failure when you have a model already -```javascript - let profile = make('profile'); - mockFindRecord(profile).fails(); - // mock.get('id') === profile.id -``` -- To use adapterOptions -```javascript - let mock = mockFindRecord('user').adapterOptions({friendly: true}); - // used when urlForFindRecord (defined in adapter) uses them - urlForFindRecord(id, modelName, snapshot) { - if (snapshot && snapshot.adapterOptions) { - let { adapterOptions } = snapshot; // => {friendly: true} - // ... blah blah blah - } - // ... blah blah - } -``` -##### `mockFindAll` - - For dealing with finding all records for a model type => `store.findAll(modelType)` - - Takes same parameters as [makeList](#factoryguymakelist) - - `mockFindAll`( fixture or model name, optional number, optional traits, optional attributes object) - - Takes modifier method `returns()` for controlling the response payload - - returns( models / json / ids ) - - Takes modifier method `adapterOptions()` for setting adapterOptions ( get passed to urlForFindAll ) - - used just as in mockFindRecord ( see example there ) - - Sample acceptance tests using `mockFindAll`: [users-view-test.js](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/acceptance/users-view-test.js) - -Usage: - -```javascript - import { buildList, makeList, mockFindAll } from 'ember-data-factory-guy'; -``` -- To mock and return no results -```javascript - let mock = mockFindAll('user'); -``` -- Using `returns({json})` to return json object -```javascript - // that has 2 different users: - let users = buildList('user', 'whacky', 'silly'); - let mock = mockFindAll('user').returns({ json: users }); - let user1 = users.get(0); - let user2 = users.get(1); - // user1.style => 'whacky' - // user2.style => 'silly' - - // or to acccomplish the same thing with less code - let mock = mockFindAll('user', 'whacky', 'silly'); - let user1 = mock.get(0); - let user2 = mock.get(1); - // user1.style => 'whacky' - // user2.style => 'silly' -``` - - Using `returns({models})` to return model instances -```javascript - let users = makeList('user', 'whacky', 'silly'); - let mock = mockFindAll('user').returns({ models: users }); - let user1 = users[0]; - // you can now also user1.get('any-computed-property') - // since you have a real model instance -``` -- To reuse the mock and return different payload -```javascript - let users2 = buildList('user', 3); - mock.returns({ json: user2 }); -``` -- To mock failure case use `fails()` method -```javascript - mockFindAll('user').fails(); -``` - -##### `mockReload` - - To handle reloading a model - - Pass in a record ( or a typeName and id ) - -Usage: - -- Passing in a record / model instance -```javascript - let profile = make('profile'); - mockReload(profile); - - // will stub a call to reload that profile - profile.reload() -``` -- Using `returns({attrs})` to return new attributes -```javascript - let profile = make('profile', { description: "whatever" }); - mockReload(profile).returns({ attrs: { description: "moo" } }); - profile.reload(); // description is now "moo" -``` -- Using `returns({json})` to return all new attributes -```javascript - let profile = make('profile', { description: "tomatoes" }); - // all new values EXCEPT the profile id ( you should keep that id the same ) - let profileAllNew = build('profile', { id: profile.get('id'), description: "potatoes" } - mockReload(profile).returns({ json: profileAllNew }); - profile.reload(); // description = "potatoes" -``` -- Mocking a failed reload -```javascript - mockReload('profile', 1).fails(); -``` - -##### `mockQuery` - - For dealing with querying for all records for a model type => `store.query(modelType, params)` - - Takes modifier method `returns()` for controlling the response payload - - returns( models / json / ids ) - - Takes modifier methods for matching the query params - - `withParams( object )` - - `withSomeParams( object )` - - Sample acceptance tests using `mockQuery`: [user-search-test.js](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/acceptance/user-search-test.js) - -Usage: - -```javascript - import FactoryGuy, { make, build, buildList, mockQuery } from 'ember-data-factory-guy'; - let store = FactoryGuy.store; - - // This simulates a query that returns no results - mockQuery('user', {age: 10}); - - store.query('user', {age: 10}}).then((userInstances) => { - /// userInstances will be empty - }) -``` - - - with returns( models ) -```javascript - // Create model instances - let users = makeList('user', 2, 'with_hats'); - - mockQuery('user', {name:'Bob', age: 10}).returns({models: users}); - - store.query('user', {name:'Bob', age: 10}}).then((models)=> { - // models are the same as the users array - }); -``` - - - with returns ( json ) -``` js - // Create json with buildList - let users = buildList('user', 2, 'with_hats'); - - mockQuery('user', {name:'Bob', age: 10}).returns({json: users}); - - store.query('user', {name:'Bob', age: 10}}).then((models)=> { - // these models were created from the users json - }); -``` - - - with returns( ids ) -```javascript - // Create list of models - let users = buildList('user', 2, 'with_hats'); - let user1 = users.get(0); - - mockQuery('user', {name:'Bob', age: 10}).returns({ids: [user1.id]}); - - store.query('user', {name:'Bob', age: 10}}).then(function(models) { - // models will be one model and it will be user1 - }); - -``` - - - withParams() / withSomeParams() -```javascript - // Create list of models - let users = buildList('user', 2, 'with_hats'); - let user1 = users.get(0); - - mock = mockQuery('user').returns({ids: [user1.id]}); - - mock.withParams({name:'Bob', age: 10}) - - // When using 'withParams' modifier, params hash must match exactly - store.query('user', {name:'Bob', age: 10}}).then(function(models) { - // models will be one model and it will be user1 - }); - - // The following call will not be caught by the mock - store.query('user', {name:'Bob', age: 10, hair: 'brown'}}) - - // 'withSomeParams' is designed to catch requests by partial match - // It has precedence over strict params matching once applied - mock.withSomeParams({name:'Bob'}) - - // Now both requests will be intercepted - store.query('user', {name:'Bob', age: 10}}) - store.query('user', {name:'Bob', age: 10, hair: 'brown'}}) -``` - -##### `mockQueryRecord` - - For dealing with querying for one record for a model type => `store.queryRecord(modelType, params)` - - takes modifier method `returns()` for controlling the response payload - - returns( model / json / id ) - - takes modifier methods for matching the query params - - withParams( object ) - -Usage: - -```javascript - import FactoryGuy, { make, build, mockQueryRecord } from 'ember-data-factory-guy'; - let store = FactoryGuy.store; - - // This simulates a query that returns no results - mockQueryRecord('user', {age: 10}); - - store.queryRecord('user', {age: 10}}).then((userInstance) => { - /// userInstance will be empty - }) -``` - - - with returns( models ) -```javascript - // Create model instances - let user = make('user'); - - mockQueryRecord('user', {name:'Bob', age: 10}).returns({model: user}); - - store.queryRecord('user', {name:'Bob', age: 10}}).then((model)=> { - // model is the same as the user you made - }); -``` - - - with returns( json ) -``` js - // Create json with buildList - let user = build('user'); - - mockQueryRecord('user', {name:'Bob', age: 10}).returns({json: user}); - - store.queryRecord('user', {name:'Bob', age: 10}}).then((model)=> { - // user model created from the user json - }); -``` - - - with returns( ids ) - -```javascript - // Create list of models - let user = build('user', 'with_hats'); - - mockQueryRecord('user', {name:'Bob', age: 10}).returns({id: user.get('id')}); - - store.queryRecord('user', {name:'Bob', age: 10}}).then(function(model) { - // model will be one model and it will be user1 - }); - -``` - -##### `mockCreate` - - - Use chainable methods to build the response - - match: takes a hash with attributes or a matching function - 1. attributes that must be in request json - - These will be added to the response json automatically, so - you don't need to include them in the returns hash. - - If you match on a `belongsTo` association, you don't have to include that in - the returns hash either ( same idea ) - 1. a function that can be used to perform an arbitrary match against the request - json, returning `true` if there is a match, `false` otherwise. - - returns - - attributes ( including relationships ) to include in response json - - Need to import `run` from `@ember/runloop` and wrap tests using `mockCreate` with: `run(function() { 'your test' })` - -Realistically, you will have code in a view action or controller action that will - create the record, and setup any associations. - -```javascript - - // most actions that create a record look something like this: - action: { - addProject: function (user) { - let name = this.$('button.project-name').val(); - this.store.createRecord('project', {name: name, user: user}).save(); - } - } - -``` - -In this case, you are are creating a 'project' record with a specific name, and belonging -to a particular user. To mock this `createRecord` call here are a few ways to do this using -chainable methods. - -Usage: - -```javascript - import { makeNew, mockCreate } from 'ember-data-factory-guy'; - - // Simplest case - // Don't care about a match just handle createRecord for any project - mockCreate('project'); - - // use a model you created already from store.createRecord or makeNew - // need to use this style if you need the model in the urlForCreateRecord snapshot - let project = makeNew('project'); - mockCreate(project); - - // Matching some attributes - mockCreate('project').match({name: "Moo"}); - - // Match all attributes - mockCreate('project').match({name: "Moo", user: user}); - - // Match using a function that checks that the request's top level attribute "name" equals 'Moo' - mockCreate('project').match(requestData => requestData.name === 'Moo'); - - // Exactly matching attributes, and returning extra attributes - mockCreate('project') - .match({name: "Moo", user: user}) - .returns({created_at: new Date()}); - - // Returning belongsTo relationship. Assume outfit belongsTo 'person' - let person = build('super-hero'); // it's polymorphic - mockCreate('outfit').returns({attrs: { person }}); - - // Returning hasMany relationship. Assume super-hero hasMany 'outfits' - let outfits = buildList('outfit', 2); - mockCreate('super-hero').returns({attrs: { outfits }}); - -``` - - - mocking a failed create - -```javascript - - // Mocking failure case is easy with chainable methods, just use #fails - mockCreate('project').match({name: "Moo"}).fails(); - - // Can optionally add a status code and/or errors to the response - mockCreate('project').fails({status: 422, response: {errors: {name: ['Moo bad, Bahh better']}}}); - - store.createRecord('project', {name: "Moo"}).save(); //=> fails -``` - - -##### `mockUpdate` - - - `mockUpdate(model)` - - Single argument ( the model instance that will be updated ) - - `mockUpdate(modelType, id)` - - Two arguments: modelType ( like 'profile' ) , and the profile id that will updated - - Use chainable methods to help build response: - - `match`: takes a hash with attributes or a matching function - 1. attributes with values that must be present on the model you are updating - 1. a function that can be used to perform an arbitrary match against the request - json, returning `true` if there is a match, `false` otherwise. - - returns - - attributes ( including relationships ) to include in response json - - Need to import `run` from `@ember/runloop` and wrap tests using `mockUpdate` with: `run(function() { 'your test' })` - -Usage: - -```javascript - import { make, mockUpdate } from 'ember-data-factory-guy'; - - let profile = make('profile'); - - // Pass in the model that will be updated ( if you have it available ) - mockUpdate(profile); - - // If the model is not available, pass in the modelType and the id of - // the model that will be updated - mockUpdate('profile', 1); - - profile.set('description', 'good value'); - profile.save() //=> will succeed - - // Returning belongsTo relationship. Assume outfit belongsTo 'person' - let outfit = make('outfit'); - let person = build('super-hero'); // it's polymorphic - outfit.set('name','outrageous'); - mockUpdate(outfit).returns({attrs: { person }}); - outfit.save(); //=> saves and returns superhero - - // Returning hasMany relationship. Assume super-hero hasMany 'outfits' - let superHero = make('super-hero'); - let outfits = buildList('outfit', 2, {name:'bell bottoms'}); - superHero.set('style','laid back'); - mockUpdate(superHero).returns({attrs: { outfits }}); - superHero.save(); // => saves and returns outfits - - // using match() method to specify attribute values - let profile = make('profile'); - profile.set('name', "woo"); - let mock = mockUpdate(profile).match({name: "moo"}); - profile.save(); // will not be mocked since the mock you set says the name must be "woo" - - // using match() method to specify a matching function - let profile = make('profile'); - profile.set('name', "woo"); - let mock = mockUpdate(profile).match((requestBody) => { - // this example uses a JSONAPI Adapter - return requestBody.data.attributes.name === "moo" - }); - profile.save(); // will not be mocked since the mock you set requires the request's top level attribute "name" to equal "moo" - - // either set the name to "moo" which will now be mocked correctly - profile.set('name', "moo"); - profile.save(); // succeeds - - // or - - // keep the profile name as "woo" - // but change the mock to match the name "woo" - mock.match({name: "woo"}); - profile.save(); // succeeds -```` - - - mocking a failed update - -```javascript - let profile = make('profile'); - - // set the succeed flag to 'false' - mockUpdate('profile', profile.id).fails({status: 422, response: 'Invalid data'}); - // or - mockUpdate(profile).fails({status: 422, response: 'Invalid data'}); - - profile.set('description', 'bad value'); - profile.save() //=> will fail -```` - -*mocking a failed update and retry with success* - -```javascript - let profile = make('profile'); - - let mockUpdate = mockUpdate(profile); - - mockUpdate.fails({status: 422, response: 'Invalid data'}); - - profile.set('description', 'bad value'); - profile.save() //=> will fail - - // After setting valid value - profile.set('description', 'good value'); - - // Now expecting success - mockUpdate.succeeds(); - - // Try that update again - profile.save() //=> will succeed! -```` - - -##### `mockDelete` - - Need to import `run` from `@ember/runloop` and wrap tests using `mockDelete` with: `run(function() { 'your test' })` - - To handle deleting a model - - Pass in a record ( or a typeName and id ) - -Usage: - -- Passing in a record / model instance - -```javascript - import { make, mockDelete } from 'ember-data-factory-guy'; - - let profile = make('profile'); - mockDelete(profile); - - profile.destroyRecord() // => will succeed -``` - -- Passing in a model typeName and id - -```javascript - import { make, mockDelete } from 'ember-data-factory-guy'; - - let profile = make('profile'); - mockDelete('profile', profile.id); - - profile.destroyRecord() // => will succeed -``` - -- Passing in a model typeName - -```javascript - import { make, mockDelete } from 'ember-data-factory-guy'; - - let profile1 = make('profile'); - let profile2 = make('profile'); - mockDelete('profile'); - - profile1.destroyRecord() // => will succeed - profile2.destroyRecord() // => will succeed -``` - -- Mocking a failed delete - -```javascript - mockDelete(profile).fails(); -``` - -##### `mock` - Well, you have read about all the other `mock*` methods, but what if you have - endpoints that do not use Ember Data? Well, `mock` is for you. - - - mock({type, url, responseText, status}) - - type: The HTTP verb (`GET`, `POST`, etc.) Defaults to `GET` - - url: The endpoint URL you are trying to mock - - responseText: This can be whatever you want to return, even a JavaScript object - - status: The status code of the response. Defaults to `200` - -Usage: - -- Simple case - -```javascript - import { mock } from 'ember-data-factory-guy'; - - this.mock({ url: '/users' }); -``` - -- Returning a JavaScript object - -```javascript - import { mock } from 'ember-data-factory-guy'; - - this.mock({ - type: 'POST', - url: '/users/sign_in', - responseText: { token: "0123456789-ab" } - }); - -``` - -### Pretender -The addon uses [Pretender](https://github.com/pretenderjs/pretender) to mock the requests. It exposes the functions `getPretender` and `setPretender` to respectively get the Pretender server for the current test or set it. For instance, you can use pretender's [passthrough](https://github.com/pretenderjs/pretender#pass-through) feature to ignore data URLs: - -```javascript -import { getPretender } from 'ember-data-factory-guy'; - -// Passthrough 'data:' requests. -getPretender().get('data:*', getPretender().passthrough); -``` - -### Tips and Tricks - -#### Tip 1: Fun with `makeList`/`buildList` and traits - - This is probably the funnest thing in FactoryGuy, if you're not using this - syntax yet, you're missing out. - - ```javascript - let json = buildList('widget', 'square', 'round', ['round','broken']); - let widgets = makeList('widget', 'square', 'round', ['round','broken']); - let [squareWidget, roundWidget, roundBrokenWidget] = widgets; - ``` - - you just built/made 3 different widgets from traits ('square', 'round', 'broken') - - the first will have the square trait - - the second will have the round trait - - the third will have both round and broken trait - - - Check out [makeList](#factoryguymakelist) and [buildList](#factoryguybuildlist) for more ideas - -#### Tip 2: Building static / fixture like data into the factories. - - - States are the classic case. There is a state model, and there are 50 US states. - - You could use a strategy to get them with traits like this: - -```javascript - import FactoryGuy from 'ember-data-factory-guy'; - - FactoryGuy.define('state', { - - traits: { - NY: { name: "New York", id: "NY" }, - NJ: { name: "New Jersey", id: "NJ" }, - CT: { name: "Connecticut", id: "CT" } - } - }); - - // then in your tests you would do - let [ny, nj, ct] = makeList('state', 'ny', 'nj', 'ct'); -``` -- Or you could use a strategy to get them like this: - -```javascript - import FactoryGuy from 'ember-data-factory-guy'; - - const states = [ - { name: "New York", id: "NY" }, - { name: "New Jersey", id: "NJ" }, - { name: "Connecticut", id: "CT" } - ... blah .. blah .. blah - ]; - - FactoryGuy.define('state', { - - default: { - id: FactoryGuy.generate((i)=> states[i-1].id), - name: FactoryGuy.generate((i)=> states[i-1].name) - } - }); - - // then in your tests you would do - let states = makeList('state', 3); // or however many states you have -``` - -#### Tip 3: Using Scenario class in tests - - encapsulate data interaction in a scenario class - - sets up data - - has helper methods to retrieve data - - similar to how page objects abstract away the interaction with a page/component - -Example: - -```javascript -// file: tests/scenarios/admin.js -import Ember from 'ember'; -import {Scenario} from 'ember-data-factory-guy'; - -export default class extends Scenario { - - run() { - this.createGroups(); - } - - createGroups() { - this.permissionGroups = this.makeList('permission-group', 3); - } - - groupNames() { - return this.permissionGroups.mapBy('name').sort(); - } -} - -// file: tests/acceptance/admin-view-test.js -import page from '../pages/admin'; -import Scenario from '../scenarios/admin'; - -describe('Admin View', function() { - let scenario; - - beforeEach(function() { - scenario = new Scenario(); - scenario.run(); - }); - - describe('group', function() { - beforeEach(function() { - page.visitGroups(); - }); - - it('shows all groups', function() { - expect(page.groups.names).to.arrayEqual(scenario.groupNames()); - }); - }); -}); -``` - -#### Tip 4: Testing mocks ( async testing ) in unit tests - - - Two ways to handle asyncronous test - - async / await ( most elegant ) [Sample test](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/unit/models/user-test.js#L44) - - need to declare polyfill for ember-cli-babel options - in [ember-cli-build](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/ember-cli-build.js#L7) - - using `assert.async()` (qunit) / `done` (mocha) [Sample test](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/unit/models/user-test.js#L53) - -#### Tip 5: Testing model's custom `serialize()` method - - The fact that you can match on attributes in `mockUpdate` and `mockCreate` means - that you can test a custom `serialize()` method in a model serializer - -```javascript - - // app/serializers/person.js - export default class PersonSerializer extends RESTSerializer { - - // let's say you're modifying all names to be Japanese honorific style - serialize(snapshot, options) { - var json = this._super(snapshot, options); - - let honorificName = [snapshot.record.get('name'), 'san'].join('-'); - json.name = honorificName; - - return json; - } - } - - // somewhere in your tests - let person = make('person', {name: "Daniel"}); - mockUpdate(person).match({name: "Daniel-san"}); - person.save(); // will succeed - // and voila, you have just tested the serializer is converting the name properly -``` - -- You could also test ```serialize()``` method in a simpler way by doing this: - -```javascript - let person = make('person', {name: "Daniel"}); - let json = person.serialize(); - assert.equal(json.name, 'Daniel-san'); -``` - -### ChangeLog - - [Release Notes](/releases) +see [`CONTRIBUTING.md`](CONTRIBUTING.md) +## License +This project is licensed under the [MIT License](LICENSE). diff --git a/docs/_sidebar.md b/docs/_sidebar.md index aa7b8d49..c11133f5 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -1,2 +1,8 @@ - Getting started - [Overview](overview.md) + - [Quick Start](quick-start.md) + - [Setup](setup.md) + - [Defining factories](defining-factories.md) + - [Using factories](using-factories.md) + - [Mocking the store](using-mock-methods.md) + - [Advanced](advanced.md) diff --git a/docs/advanced.md b/docs/advanced.md new file mode 100644 index 00000000..86904c99 --- /dev/null +++ b/docs/advanced.md @@ -0,0 +1,393 @@ +# Advanced + +## 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 Model.extend({ + name: fragment('name'), + phoneNumbers: fragmentArray('phone-number') +}) + +// Name fragment +export default Fragment.extend({ + titles: array('string'), + firstName: attr('string'), + lastName: attr('string') +}); + +// Phone Number fragment +export default Fragment.extend({ + number: attr('string') + type: attr('string') +}); +``` + +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 `addon-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()` + +## Tips and Tricks + +### Tip 1: Fun with `makeList`/`buildList` and traits + +- This is probably the funnest thing in FactoryGuy, if you're not using this + syntax yet, you're missing out. + +```javascript +let json = buildList('widget', 'square', 'round', ['round', 'broken']); +let widgets = makeList('widget', 'square', 'round', ['round', 'broken']); +let [squareWidget, roundWidget, roundBrokenWidget] = widgets; +``` + + - you just built/made 3 different widgets from traits ('square', 'round', 'broken') + - the first will have the square trait + - the second will have the round trait + - the third will have both round and broken trait + +### Tip 2: Building static / fixture like data into the factories. + +- States are the classic case. There is a state model, and there are 50 US states. +- You could use a strategy to get them with traits like this: + +```javascript +import FactoryGuy from 'ember-data-factory-guy'; + +FactoryGuy.define('state', { + traits: { + NY: { name: 'New York', id: 'NY' }, + NJ: { name: 'New Jersey', id: 'NJ' }, + CT: { name: 'Connecticut', id: 'CT' }, + }, +}); + +// then in your tests you would do +let [ny, nj, ct] = makeList('state', 'ny', 'nj', 'ct'); +``` + +- Or you could use a strategy to get them like this: + +```javascript + import FactoryGuy from 'ember-data-factory-guy'; + + const states = [ + { name: "New York", id: "NY" }, + { name: "New Jersey", id: "NJ" }, + { name: "Connecticut", id: "CT" } + ... blah .. blah .. blah + ]; + + FactoryGuy.define('state', { + + default: { + id: FactoryGuy.generate((i)=> states[i-1].id), + name: FactoryGuy.generate((i)=> states[i-1].name) + } + }); + + // then in your tests you would do + let states = makeList('state', 3); // or however many states you have +``` + +### Tip 3: Using Scenario class in tests + +- encapsulate data interaction in a scenario class + - sets up data + - has helper methods to retrieve data +- similar to how page objects abstract away the interaction with a page/component + +Example: + +```javascript +// file: tests/scenarios/admin.js +import Ember from 'ember'; +import { Scenario } from 'ember-data-factory-guy'; + +export default class extends Scenario { + run() { + this.createGroups(); + } + + createGroups() { + this.permissionGroups = this.makeList('permission-group', 3); + } + + groupNames() { + return this.permissionGroups.mapBy('name').sort(); + } +} + +// file: tests/acceptance/admin-view-test.js +import page from '../pages/admin'; +import Scenario from '../scenarios/admin'; + +describe('Admin View', function () { + let scenario; + + beforeEach(function () { + scenario = new Scenario(); + scenario.run(); + }); + + describe('group', function () { + beforeEach(function () { + page.visitGroups(); + }); + + it('shows all groups', function () { + expect(page.groups.names).to.arrayEqual(scenario.groupNames()); + }); + }); +}); +``` + +### Tip 4: Testing mocks ( async testing ) in unit tests + +- Two ways to handle asyncronous test + - async / await ( most elegant ) [Sample test](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/unit/models/user-test.js#L44) + - need to declare polyfill for ember-cli-babel options + in [ember-cli-build](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/ember-cli-build.js#L7) + - using `assert.async()` (qunit) / `done` (mocha) [Sample test](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/unit/models/user-test.js#L53) + +### Tip 5: Testing model's custom `serialize()` method + +- The fact that you can match on attributes in `mockUpdate` and `mockCreate` means + that you can test a custom `serialize()` method in a model serializer + +```javascript +// app/serializers/person.js +export default DS.RESTSerializer.extend({ + // let's say you're modifying all names to be Japanese honorific style + serialize: function (snapshot, options) { + var json = this._super(snapshot, options); + + let honorificName = [snapshot.record.get('name'), 'san'].join('-'); + json.name = honorificName; + + return json; + }, +}); + +// somewhere in your tests +let person = make('person', { name: 'Daniel' }); +mockUpdate(person).match({ name: 'Daniel-san' }); +person.save(); // will succeed +// and voila, you have just tested the serializer is converting the name properly +``` + +- You could also test `serialize()` method in a simpler way by doing this: + +```javascript +let person = make('person', { name: 'Daniel' }); +let json = person.serialize(); +assert.equal(json.name, 'Daniel-san'); +``` + +## 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 +}); +``` diff --git a/docs/defining-factories.md b/docs/defining-factories.md new file mode 100644 index 00000000..36c5930b --- /dev/null +++ b/docs/defining-factories.md @@ -0,0 +1,452 @@ +A factory is defined by a name and a set of attributes, with the name helping find the corresponding model. This means +that our `User` model's factory must be named `user`. + +Factories are created in the `tests/factories/` directory of your application and can be generated using the followin +command: + +```bash +ember generate factory +``` + +This will create a file with the following path: `tests/factories/.js` with the following content: + +```javascript +import FactoryGuy from 'ember-data-factory-guy'; + +FactoryGuy.define('', { + default: {}, +}); +``` + +The `default` object is used to define the default attributes' values the model will be fed with when created. For our +`user` factory, it would look like: + +```javascript +import FactoryGuy from 'ember-data-factory-guy'; + +FactoryGuy.define('user', { + default: { + style: 'normal', + name: 'Dude', + }, +}); +``` + +Additionally to the `default` object, a factory's attributes can be defined by defining "named" attribute groups as +shortcuts to passing the attributes manually when creating the factory. + +```javascript +import FactoryGuy from 'ember-data-factory-guy'; + +FactoryGuy.define('user', { + default: { + style: 'normal', + name: 'Dude', + }, + + admin: { + style: 'super', + name: 'Admin', + }, +}); +``` + +Attributes can also be defined using **traits**, on which we will go through later in this documentation. + +> Disabling polymorphism +> +> If your model happens to define a `type` attribute but is not considered a polymorphic model, you will need to enforce +> that on your factory definition by setting the `polymorphic` key to `false`. + +```javascript +FactoryGuy.define('cat', { + polymorphic: false, // Prevent FactoryGuy from treating this model as polymorphic. + default: { + type: 'Cute', + name: (f) => `Cat ${f.id}`, + }, +}); +``` + +## Using sequences + +Sequences helps you generate unique attribute values either by declaring them first (useful if you need to use the +sequence for multiple attributes), or by declaring them inline where needed. In both cases, the values are generated +by calling the `FactoryGuy.generate` function. + + + +#### **Shared sequence** + +```javascript +import FactoryGuy from 'ember-data-factory-guy'; + +FactoryGuy.define('user', { + sequences: { + userName: (num) => `User #${num}`, + }, + default: { + style: 'normal', + name: FactoryGuy.generate('userName'), + }, +}); + +const firstUser = FactoryGuy.make('user'); +const secondUser = FactoryGuy.make('user'); + +firstUser.get('name'); // => User #1 +secondUser.get('name'); // => User #2 +``` + +#### **Inline sequence** + +```javascript +import FactoryGuy from 'ember-data-factory-guy'; + +FactoryGuy.define('user', { + default: { + style: 'normal', + name: FactoryGuy.generate((num) => `User #${num}`), + }, +}); + +const firstUser = FactoryGuy.make('user'); +const secondUser = FactoryGuy.make('user'); + +firstUser.get('name'); // => User #1 +secondUser.get('name'); // => User #2 +``` + + + +## Using inline functions + +To get more control over the data set for an attribute, you can use a function to declare it. This allows you to add +some logic defining what value should be returned depending on the other attributes of the factory. To do so, the function +is provided with the fixture itself so you can reference all other attributes. + +```javascript +FactoryGuy.define('user', { + default: { + // The id being almost always a sequential number, you can use it directly in the inline function instead of + // using a name sequence. + name: (userFixture) => `User #${userFixture.id}`, + }, +}); + +const user = FactoryGuy.make('user'); + +user.get('id'); // => 1 +user.get('name'); // => 'User #1' +``` + +## Defining relationships + +Just like with models, you can define relationships in your factories using the `FactoryGuy.belongsTo` and +`FactoryGuy.hasMany` functions. Alike attributes, they can be defined using inline definitions, with traits, but +also as links for async relationships. + +Relationships can be used when creating a factory record using the `FactoryGuy.build`, `FactoryGuy.buildList`, +`FactoryGuy.make`, `FactoryGuy.makeNew` and `Factory.makeList` build strategies. + +When defining a factory, relationships are better setup using traits like: + + + +### **belongsTo relationships** + +```javascript +FactoryGuy.define('project', { + traits: { + withUser: { user: FactoryGuy.belongsTo('user') }, + withAdmin: { user: FactoryGuy.belongsTo('user', 'admin') }, + withManagerLink(f) { + f.links = { manager: `/projects/${f.id}/manager` }; + }, + }, +}); + +const project = make('project', 'withUser'); +project.user.toJSON({ includeId: true }); // => {id:1, name: 'Dude', style: 'normal'} + +const user = make('user', 'withManagerLink'); +user.belongsTo('manager').link(); // => "/projects/1/manager" +``` + +### **hasMany relationships** + +```javascript +FactoryGuy.define('user', { + traits: { + withProjects: { + projects: FactoryGuy.hasMany('project', 2), + }, + withPropertiesLink(f) { + f.links = { properties: `/users/${f.id}/properties` }; + }, + }, +}); + +let user = make('user', 'withProjects'); +user.get('projects.length'); // => 2 + +user = make('user', 'withPropertiesLink'); +user.hasMany('properties').link(); // => "/users/1/properties" +``` + + + +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 + +When dealing with models with async relationships, you can setup links for them in your factory definition like: + +```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'}}); +``` + +## Traits + +Traits allow you to group attributes together and then apply them to any factory. There power also resides in that you +can compose your factory by applying multiple traits to it. Traits are declared in the factory's `traits` object. + +**Each trait overrides any values defined in traits before it in the argument list.** + +```javascript +FactoryGuy.define('user', { + traits: { + big: { name: 'Big Guy' }, + friendly: { style: 'Friendly' }, + bfg: { name: 'Big Friendly Giant', style: 'Friendly' }, + }, +}); + +const user = FactoryGuy.make('user', 'big', 'friendly'); +const giant = FactoryGuy.make('user', 'big', 'bfg'); + +user.get('name'); // => 'Big Guy' +user.get('style'); // => 'Friendly' +giant.get('name'); // => 'Big Friendly Giant' - name defined in the 'bfg' trait overrides the name defined in the 'big' trait +giant.get('style'); // => 'Friendly' +``` + +You can still pass in a hash of attributes when using traits and it 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 + +Alike attributes, traits can be defined using functions too. This is super powerful as it allows you to make your traits +even more dynamic. + +```javascript +import FactoryGuy from 'ember-data-factory-guy'; + +FactoryGuy.define('project', { + default: { + title: (f) => `Project ${f.id}`, + }, + traits: { + medium: (f) => { + f.title = `Medium Project ${f.id}`; + }, + goofy: (f) => { + f.title = `Goofy ${f.title}`; + }, + withUser: (f) => { + // NOTE: In traits, relationships are directly created instead of just declared using FactoryGuy.belongsTo as you + // would normally in a factory definition. + f.user = FactoryGuy.make('user'); + }, + }, +}); + +const project = make('project', 'medium'); +const project2 = build('project', 'goofy'); +const project3 = build('project', 'withUser'); + +project.get('title'); //=> 'Medium Project 1' +project2.get('title'); //=> 'Goofy Project 2' +project3.get('title'); //=> 'Project #3' +project3.get('user.name'); //=> 'User 1' +``` + +## 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 Model.extend({ + dogNumber: attr('string'), + sound: attr('string'), +}); + +// 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"`; + +## 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' + } + }) + +``` diff --git a/docs/index.html b/docs/index.html index 16893858..d23a7816 100644 --- a/docs/index.html +++ b/docs/index.html @@ -9,10 +9,7 @@ name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" /> - +
diff --git a/docs/overview.md b/docs/overview.md index 36267fca..1a24a7ab 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -5,3 +5,19 @@ 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. +## Features + +- 🚀 Easily push models to the Store, even the most complex relationships +- ⚡️️ Simple, Powerful, & Intuitive API +- ⭐ Works out of the box with your existing serializers and adapters +- 💎 Multiple build strategies +- 🕵️ Intercept and spy on Store requests +- 🕐 Slow down response times + +## Getting Started + +Check out the [Quick Start](quick-start.md) documentation to get started. + +## Questions / Get in Touch + +Visit the EmberJS Community on [Discord](https://discord.com/channels/480462759797063690/483601670685720591). diff --git a/docs/quick-start.md b/docs/quick-start.md new file mode 100644 index 00000000..fb1cb9f7 --- /dev/null +++ b/docs/quick-start.md @@ -0,0 +1,33 @@ +# Quick Start + +## Installation + +You can install `ember-data-factory-guy` using the Ember package installer: + +```bash +ember install ember-data-factory-guy +``` + +Or you can install it using your favorite package manager: + +```bash +npm install ember-data-factory-guy --save-dev +``` + +```bash +yarn add --dev ember-data-factory-guy +``` + +```bash +pnpm add -D ember-data-factory-guy +``` + +## How it works + +Factory Guy works by: + +- defining factories for your models +- use them to create records in the Store using the built-in build strategies. + +The build strategies allow you to either create new records, persisted ones, or just build a JSON payload for your model +for mocking an HTTP request's payload. diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 00000000..ca4fdbd6 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,45 @@ +# Setup + +Throughout this documentation,In the following examples, we will assume our application has the following models: + + + +### **models/user.js** + +```javascript +import Model, { attr, hasMany } from '@ember-data/model'; + +class User extends Model { + @attr('string') name; + @attr('string') style; + @hasMany('project') projects; + @hasMany('hat', { polymorphic: true }) hats; +} +``` + +#### **models/project.js** + +```javascript +import Model, { attr, belongsTo } from '@ember-data/model'; + +class Project extends Model { + @attr('string') title; + @belongsTo('user') user; +} +``` + +#### **models/hat.js (Polymorphic)** + +```javascript +import Model, { attr, belongsTo } from '@ember-data/model'; + +class Hat extends Model { + @attr('string') type; + @belongsTo('user') user; +} + +class BigHat extends Hat(); +class SmallHat extends Hat(); +``` + + diff --git a/docs/tips.md b/docs/tips.md new file mode 100644 index 00000000..4ffaed6b --- /dev/null +++ b/docs/tips.md @@ -0,0 +1,158 @@ + +### Tips and Tricks + +#### Tip 1: Fun with `makeList`/`buildList` and traits + +- This is probably the funnest thing in FactoryGuy, if you're not using this + syntax yet, you're missing out. + +```javascript +let json = buildList('widget', 'square', 'round', ['round', 'broken']); +let widgets = makeList('widget', 'square', 'round', ['round', 'broken']); +let [squareWidget, roundWidget, roundBrokenWidget] = widgets; +``` + + - you just built/made 3 different widgets from traits ('square', 'round', 'broken') + - the first will have the square trait + - the second will have the round trait + - the third will have both round and broken trait + +- Check out [makeList](#factoryguymakelist) and [buildList](#factoryguybuildlist) for more ideas + +#### Tip 2: Building static / fixture like data into the factories. + +- States are the classic case. There is a state model, and there are 50 US states. +- You could use a strategy to get them with traits like this: + +```javascript +import FactoryGuy from 'ember-data-factory-guy'; + +FactoryGuy.define('state', { + traits: { + NY: { name: 'New York', id: 'NY' }, + NJ: { name: 'New Jersey', id: 'NJ' }, + CT: { name: 'Connecticut', id: 'CT' }, + }, +}); + +// then in your tests you would do +let [ny, nj, ct] = makeList('state', 'ny', 'nj', 'ct'); +``` + +- Or you could use a strategy to get them like this: + +```javascript + import FactoryGuy from 'ember-data-factory-guy'; + + const states = [ + { name: "New York", id: "NY" }, + { name: "New Jersey", id: "NJ" }, + { name: "Connecticut", id: "CT" } + ... blah .. blah .. blah + ]; + + FactoryGuy.define('state', { + + default: { + id: FactoryGuy.generate((i)=> states[i-1].id), + name: FactoryGuy.generate((i)=> states[i-1].name) + } + }); + + // then in your tests you would do + let states = makeList('state', 3); // or however many states you have +``` + +#### Tip 3: Using Scenario class in tests + +- encapsulate data interaction in a scenario class + - sets up data + - has helper methods to retrieve data +- similar to how page objects abstract away the interaction with a page/component + +Example: + +```javascript +// file: tests/scenarios/admin.js +import Ember from 'ember'; +import { Scenario } from 'ember-data-factory-guy'; + +export default class extends Scenario { + run() { + this.createGroups(); + } + + createGroups() { + this.permissionGroups = this.makeList('permission-group', 3); + } + + groupNames() { + return this.permissionGroups.mapBy('name').sort(); + } +} + +// file: tests/acceptance/admin-view-test.js +import page from '../pages/admin'; +import Scenario from '../scenarios/admin'; + +describe('Admin View', function () { + let scenario; + + beforeEach(function () { + scenario = new Scenario(); + scenario.run(); + }); + + describe('group', function () { + beforeEach(function () { + page.visitGroups(); + }); + + it('shows all groups', function () { + expect(page.groups.names).to.arrayEqual(scenario.groupNames()); + }); + }); +}); +``` + +#### Tip 4: Testing mocks ( async testing ) in unit tests + +- Two ways to handle asyncronous test + - async / await ( most elegant ) [Sample test](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/unit/models/user-test.js#L44) + - need to declare polyfill for ember-cli-babel options + in [ember-cli-build](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/ember-cli-build.js#L7) + - using `assert.async()` (qunit) / `done` (mocha) [Sample test](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/unit/models/user-test.js#L53) + +#### Tip 5: Testing model's custom `serialize()` method + +- The fact that you can match on attributes in `mockUpdate` and `mockCreate` means + that you can test a custom `serialize()` method in a model serializer + +```javascript +// app/serializers/person.js +export default DS.RESTSerializer.extend({ + // let's say you're modifying all names to be Japanese honorific style + serialize: function (snapshot, options) { + var json = this._super(snapshot, options); + + let honorificName = [snapshot.record.get('name'), 'san'].join('-'); + json.name = honorificName; + + return json; + }, +}); + +// somewhere in your tests +let person = make('person', { name: 'Daniel' }); +mockUpdate(person).match({ name: 'Daniel-san' }); +person.save(); // will succeed +// and voila, you have just tested the serializer is converting the name properly +``` + +- You could also test `serialize()` method in a simpler way by doing this: + +```javascript +let person = make('person', { name: 'Daniel' }); +let json = person.serialize(); +assert.equal(json.name, 'Daniel-san'); +``` diff --git a/docs/using-factories.md b/docs/using-factories.md new file mode 100644 index 00000000..9dcf53b3 --- /dev/null +++ b/docs/using-factories.md @@ -0,0 +1,462 @@ +# Using Factories + +## Build strategies + +- [`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' }] +); +``` + +## Additional methods + +### 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"} +``` + +## Testing + +In order to use FactoryGuy in tests, it needs to be setup using the `setupFactoryGuy(hooks)` function after running your +test's setup (`setupTest`, `setupRenderingTest`, or `setupApplicationTest` depending on whether you're writing a unit, +integration, or acceptance test). + + + +#### **Acceptance test** + +```javascript +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'); + }); +}); +``` + +#### **Integration test** + +```javascript +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { make, setupFactoryGuy } from 'ember-data-factory-guy'; +import { hbs } from 'ember-cli-htmlbars'; + +module( + `Integration | Component | single-user (manual setup)`, + function (hooks) { + setupRenderingTest(hooks); + setupFactoryGuy(hooks); + + test('shows user information', async function (assert) { + let user = make('user', { name: 'Rob' }); + + this.setProperties({ user, createProject: () => {} }); + await render( + hbs`` + ); + + assert.dom('.name').containsText(user.get('name')); + assert.dom('.funny-name').containsText(user.get('funnyName')); + }); + } +); +``` + +#### **Unit test** + +```javascript +import { run } from '@ember/runloop'; +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import FactoryGuy, { + make, + setupFactoryGuy, + mockFindRecord, +} from 'ember-data-factory-guy'; + +const modelType = 'user'; + +module(`Unit | Model | ${modelType}`, function (hooks) { + setupTest(hooks); + setupFactoryGuy(hooks); + + test('has funny name', function (assert) { + let user = make('user', { name: 'Dude' }); + assert.equal(user.get('funnyName'), 'funny Dude'); + }); + + test('has projects', function (assert) { + let user = make('user', 'with_projects'); + assert.equal(user.get('projects.length'), 2); + }); + + test('sample async unit test with async/await', async function (assert) { + assert.expect(1); + let mock = mockFindRecord('user'); + let userId = mock.get('id'); + let user = await FactoryGuy.store.findRecord('user', userId); + assert.equal(user.get('name'), mock.get('name')); + }); + + test('sample async unit test with assert.async()', function (assert) { + assert.expect(1); + let done = assert.async(); + run(() => { + let mock = mockFindRecord('user'); + let userId = mock.get('id'); + FactoryGuy.store.findRecord('user', userId).then((user) => { + assert.equal(user.get('name'), mock.get('name')); + done(); + }); + }); + }); +}); +``` + + diff --git a/docs/using-mock-methods.md b/docs/using-mock-methods.md new file mode 100644 index 00000000..9e66e26c --- /dev/null +++ b/docs/using-mock-methods.md @@ -0,0 +1,753 @@ +# 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 failure when you have a model already + +```javascript +let profile = make('profile'); +mockFindRecord(profile).fails(); +// mock.get('id') === profile.id +``` + +- To use adapterOptions + +```javascript + let mock = mockFindRecord('user').adapterOptions({friendly: true}); + // used when urlForFindRecord (defined in adapter) uses them + urlForFindRecord(id, modelName, snapshot) { + if (snapshot && snapshot.adapterOptions) { + let { adapterOptions } = snapshot; // => {friendly: true} + // ... blah blah blah + } + // ... blah blah + } +``` + +## `mockFindAll` + +- For dealing with finding all records for a model type => `store.findAll(modelType)` +- Takes same parameters as [makeList](#factoryguymakelist) + - `mockFindAll`( fixture or model name, optional number, optional traits, optional attributes object) +- Takes modifier method `returns()` for controlling the response payload + - returns( models / json / ids ) +- Takes modifier method `adapterOptions()` for setting adapterOptions ( get passed to urlForFindAll ) + - used just as in mockFindRecord ( see example there ) +- Sample acceptance tests using `mockFindAll`: [users-view-test.js](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/acceptance/users-view-test.js) + +Usage: + +```javascript +import { buildList, makeList, mockFindAll } from 'ember-data-factory-guy'; +``` + +- To mock and return no results + +```javascript +let mock = mockFindAll('user'); +``` + +- Using `returns({json})` to return json object + +```javascript +// that has 2 different users: +let users = buildList('user', 'whacky', 'silly'); +let mock = mockFindAll('user').returns({ json: users }); +let user1 = users.get(0); +let user2 = users.get(1); +// user1.style => 'whacky' +// user2.style => 'silly' + +// or to acccomplish the same thing with less code +let mock = mockFindAll('user', 'whacky', 'silly'); +let user1 = mock.get(0); +let user2 = mock.get(1); +// user1.style => 'whacky' +// user2.style => 'silly' +``` + +- Using `returns({models})` to return model instances + +```javascript +let users = makeList('user', 'whacky', 'silly'); +let mock = mockFindAll('user').returns({ models: users }); +let user1 = users[0]; +// you can now also user1.get('any-computed-property') +// since you have a real model instance +``` + +- To reuse the mock and return different payload + +```javascript +let users2 = buildList('user', 3); +mock.returns({ json: user2 }); +``` + +- To mock failure case use `fails()` method + +```javascript +mockFindAll('user').fails(); +``` + +## `mockReload` + +- To handle reloading a model + - Pass in a record ( or a typeName and id ) + +Usage: + +- Passing in a record / model instance + +```javascript +let profile = make('profile'); +mockReload(profile); + +// will stub a call to reload that profile +profile.reload(); +``` + +- Using `returns({attrs})` to return new attributes + +```javascript +let profile = make('profile', { description: 'whatever' }); +mockReload(profile).returns({ attrs: { description: 'moo' } }); +profile.reload(); // description is now "moo" +``` + +- Using `returns({json})` to return all new attributes + +```javascript + let profile = make('profile', { description: "tomatoes" }); + // all new values EXCEPT the profile id ( you should keep that id the same ) + let profileAllNew = build('profile', { id: profile.get('id'), description: "potatoes" } + mockReload(profile).returns({ json: profileAllNew }); + profile.reload(); // description = "potatoes" +``` + +- Mocking a failed reload + +```javascript +mockReload('profile', 1).fails(); +``` + +## `mockQuery` + +- For dealing with querying for all records for a model type => `store.query(modelType, params)` + - Takes modifier method `returns()` for controlling the response payload + - returns( models / json / ids ) +- Takes modifier methods for matching the query params +- `withParams( object )` +- `withSomeParams( object )` +- Sample acceptance tests using `mockQuery`: [user-search-test.js](https://github.com/adopted-ember-addons/ember-data-factory-guy/blob/master/tests/acceptance/user-search-test.js) + +Usage: + +```javascript + import FactoryGuy, { make, build, buildList, mockQuery } from 'ember-data-factory-guy'; + let store = FactoryGuy.store; + + // This simulates a query that returns no results + mockQuery('user', {age: 10}); + + store.query('user', {age: 10}}).then((userInstances) => { + /// userInstances will be empty + }) +``` + +- with returns( models ) + +```javascript + // Create model instances + let users = makeList('user', 2, 'with_hats'); + + mockQuery('user', {name:'Bob', age: 10}).returns({models: users}); + + store.query('user', {name:'Bob', age: 10}}).then((models)=> { + // models are the same as the users array + }); +``` + +- with returns ( json ) + +```js + // Create json with buildList + let users = buildList('user', 2, 'with_hats'); + + mockQuery('user', {name:'Bob', age: 10}).returns({json: users}); + + store.query('user', {name:'Bob', age: 10}}).then((models)=> { + // these models were created from the users json + }); +``` + +- with returns( ids ) + +```javascript + // Create list of models + let users = buildList('user', 2, 'with_hats'); + let user1 = users.get(0); + + mockQuery('user', {name:'Bob', age: 10}).returns({ids: [user1.id]}); + + store.query('user', {name:'Bob', age: 10}}).then(function(models) { + // models will be one model and it will be user1 + }); + +``` + +- withParams() / withSomeParams() + +```javascript + // Create list of models + let users = buildList('user', 2, 'with_hats'); + let user1 = users.get(0); + + mock = mockQuery('user').returns({ids: [user1.id]}); + + mock.withParams({name:'Bob', age: 10}) + + // When using 'withParams' modifier, params hash must match exactly + store.query('user', {name:'Bob', age: 10}}).then(function(models) { + // models will be one model and it will be user1 + }); + + // The following call will not be caught by the mock + store.query('user', {name:'Bob', age: 10, hair: 'brown'}}) + + // 'withSomeParams' is designed to catch requests by partial match + // It has precedence over strict params matching once applied + mock.withSomeParams({name:'Bob'}) + + // Now both requests will be intercepted + store.query('user', {name:'Bob', age: 10}}) + store.query('user', {name:'Bob', age: 10, hair: 'brown'}}) +``` + +## `mockQueryRecord` + +- For dealing with querying for one record for a model type => `store.queryRecord(modelType, params)` + - takes modifier method `returns()` for controlling the response payload + - returns( model / json / id ) +- takes modifier methods for matching the query params +- withParams( object ) + +Usage: + +```javascript + import FactoryGuy, { make, build, mockQueryRecord } from 'ember-data-factory-guy'; + let store = FactoryGuy.store; + + // This simulates a query that returns no results + mockQueryRecord('user', {age: 10}); + + store.queryRecord('user', {age: 10}}).then((userInstance) => { + /// userInstance will be empty + }) +``` + +- with returns( models ) + +```javascript + // Create model instances + let user = make('user'); + + mockQueryRecord('user', {name:'Bob', age: 10}).returns({model: user}); + + store.queryRecord('user', {name:'Bob', age: 10}}).then((model)=> { + // model is the same as the user you made + }); +``` + +- with returns( json ) + +```js + // Create json with buildList + let user = build('user'); + + mockQueryRecord('user', {name:'Bob', age: 10}).returns({json: user}); + + store.queryRecord('user', {name:'Bob', age: 10}}).then((model)=> { + // user model created from the user json + }); +``` + +- with returns( ids ) + +```javascript + // Create list of models + let user = build('user', 'with_hats'); + + mockQueryRecord('user', {name:'Bob', age: 10}).returns({id: user.get('id')}); + + store.queryRecord('user', {name:'Bob', age: 10}}).then(function(model) { + // model will be one model and it will be user1 + }); + +``` + +## `mockCreate` + +- Use chainable methods to build the response + - match: takes a hash with attributes or a matching function + 1. attributes that must be in request json + - These will be added to the response json automatically, so + you don't need to include them in the returns hash. + - If you match on a `belongsTo` association, you don't have to include that in + the returns hash either ( same idea ) + 1. a function that can be used to perform an arbitrary match against the request + json, returning `true` if there is a match, `false` otherwise. + - returns + - attributes ( including relationships ) to include in response json +- Need to import `run` from `@ember/runloop` and wrap tests using `mockCreate` with: `run(function() { 'your test' })` + +Realistically, you will have code in a view action or controller action that will +create the record, and setup any associations. + +```javascript + + // most actions that create a record look something like this: + action: { + addProject: function (user) { + let name = this.$('button.project-name').val(); + this.store.createRecord('project', {name: name, user: user}).save(); + } + } + +``` + +In this case, you are are creating a 'project' record with a specific name, and belonging +to a particular user. To mock this `createRecord` call here are a few ways to do this using +chainable methods. + +Usage: + +```javascript +import { makeNew, mockCreate } from 'ember-data-factory-guy'; + +// Simplest case +// Don't care about a match just handle createRecord for any project +mockCreate('project'); + +// use a model you created already from store.createRecord or makeNew +// need to use this style if you need the model in the urlForCreateRecord snapshot +let project = makeNew('project'); +mockCreate(project); + +// Matching some attributes +mockCreate('project').match({ name: 'Moo' }); + +// Match all attributes +mockCreate('project').match({ name: 'Moo', user: user }); + +// Match using a function that checks that the request's top level attribute "name" equals 'Moo' +mockCreate('project').match((requestData) => requestData.name === 'Moo'); + +// Exactly matching attributes, and returning extra attributes +mockCreate('project') + .match({ name: 'Moo', user: user }) + .returns({ created_at: new Date() }); + +// Returning belongsTo relationship. Assume outfit belongsTo 'person' +let person = build('super-hero'); // it's polymorphic +mockCreate('outfit').returns({ attrs: { person } }); + +// Returning hasMany relationship. Assume super-hero hasMany 'outfits' +let outfits = buildList('outfit', 2); +mockCreate('super-hero').returns({ attrs: { outfits } }); +``` + +- mocking a failed create + +```javascript +// Mocking failure case is easy with chainable methods, just use #fails +mockCreate('project').match({ name: 'Moo' }).fails(); + +// Can optionally add a status code and/or errors to the response +mockCreate('project').fails({ + status: 422, + response: { errors: { name: ['Moo bad, Bahh better'] } }, +}); + +store.createRecord('project', { name: 'Moo' }).save(); //=> fails +``` + +## `mockUpdate` + +- `mockUpdate(model)` + - Single argument ( the model instance that will be updated ) +- `mockUpdate(modelType, id)` + - Two arguments: modelType ( like 'profile' ) , and the profile id that will updated +- Use chainable methods to help build response: + - `match`: takes a hash with attributes or a matching function + 1. attributes with values that must be present on the model you are updating + 1. a function that can be used to perform an arbitrary match against the request + json, returning `true` if there is a match, `false` otherwise. + - returns + - attributes ( including relationships ) to include in response json +- Need to import `run` from `@ember/runloop` and wrap tests using `mockUpdate` with: `run(function() { 'your test' })` + +Usage: + +```javascript +import { make, mockUpdate } from 'ember-data-factory-guy'; + +let profile = make('profile'); + +// Pass in the model that will be updated ( if you have it available ) +mockUpdate(profile); + +// If the model is not available, pass in the modelType and the id of +// the model that will be updated +mockUpdate('profile', 1); + +profile.set('description', 'good value'); +profile.save(); //=> will succeed + +// Returning belongsTo relationship. Assume outfit belongsTo 'person' +let outfit = make('outfit'); +let person = build('super-hero'); // it's polymorphic +outfit.set('name', 'outrageous'); +mockUpdate(outfit).returns({ attrs: { person } }); +outfit.save(); //=> saves and returns superhero + +// Returning hasMany relationship. Assume super-hero hasMany 'outfits' +let superHero = make('super-hero'); +let outfits = buildList('outfit', 2, { name: 'bell bottoms' }); +superHero.set('style', 'laid back'); +mockUpdate(superHero).returns({ attrs: { outfits } }); +superHero.save(); // => saves and returns outfits + +// using match() method to specify attribute values +let profile = make('profile'); +profile.set('name', 'woo'); +let mock = mockUpdate(profile).match({ name: 'moo' }); +profile.save(); // will not be mocked since the mock you set says the name must be "woo" + +// using match() method to specify a matching function +let profile = make('profile'); +profile.set('name', 'woo'); +let mock = mockUpdate(profile).match((requestBody) => { + // this example uses a JSONAPI Adapter + return requestBody.data.attributes.name === 'moo'; +}); +profile.save(); // will not be mocked since the mock you set requires the request's top level attribute "name" to equal "moo" + +// either set the name to "moo" which will now be mocked correctly +profile.set('name', 'moo'); +profile.save(); // succeeds + +// or + +// keep the profile name as "woo" +// but change the mock to match the name "woo" +mock.match({ name: 'woo' }); +profile.save(); // succeeds +``` + +- mocking a failed update + +```javascript +let profile = make('profile'); + +// set the succeed flag to 'false' +mockUpdate('profile', profile.id).fails({ + status: 422, + response: 'Invalid data', +}); +// or +mockUpdate(profile).fails({ status: 422, response: 'Invalid data' }); + +profile.set('description', 'bad value'); +profile.save(); //=> will fail +``` + +_mocking a failed update and retry with success_ + +```javascript +let profile = make('profile'); + +let mockUpdate = mockUpdate(profile); + +mockUpdate.fails({ status: 422, response: 'Invalid data' }); + +profile.set('description', 'bad value'); +profile.save(); //=> will fail + +// After setting valid value +profile.set('description', 'good value'); + +// Now expecting success +mockUpdate.succeeds(); + +// Try that update again +profile.save(); //=> will succeed! +``` + +## `mockDelete` + +- Need to import `run` from `@ember/runloop` and wrap tests using `mockDelete` with: `run(function() { 'your test' })` +- To handle deleting a model + - Pass in a record ( or a typeName and id ) + +Usage: + +- Passing in a record / model instance + +```javascript +import { make, mockDelete } from 'ember-data-factory-guy'; + +let profile = make('profile'); +mockDelete(profile); + +profile.destroyRecord(); // => will succeed +``` + +- Passing in a model typeName and id + +```javascript +import { make, mockDelete } from 'ember-data-factory-guy'; + +let profile = make('profile'); +mockDelete('profile', profile.id); + +profile.destroyRecord(); // => will succeed +``` + +- Passing in a model typeName + +```javascript +import { make, mockDelete } from 'ember-data-factory-guy'; + +let profile1 = make('profile'); +let profile2 = make('profile'); +mockDelete('profile'); + +profile1.destroyRecord(); // => will succeed +profile2.destroyRecord(); // => will succeed +``` + +- Mocking a failed delete + +```javascript +mockDelete(profile).fails(); +``` + +## `mock` + +Well, you have read about all the other `mock*` methods, but what if you have +endpoints that do not use Ember Data? Well, `mock` is for you. + +- mock({type, url, responseText, status}) + - type: The HTTP verb (`GET`, `POST`, etc.) Defaults to `GET` + - url: The endpoint URL you are trying to mock + - responseText: This can be whatever you want to return, even a JavaScript object + - status: The status code of the response. Defaults to `200` + +Usage: + +- Simple case + +```javascript +import { mock } from 'ember-data-factory-guy'; + +this.mock({ url: '/users' }); +``` + +- Returning a JavaScript object + +```javascript +import { mock } from 'ember-data-factory-guy'; + +this.mock({ + type: 'POST', + url: '/users/sign_in', + responseText: { token: '0123456789-ab' }, +}); +``` + +## Using Pretender + +The addon uses [Pretender](https://github.com/pretenderjs/pretender) to mock the requests. It exposes the functions `getPretender` and `setPretender` to respectively get the Pretender server for the current test or set it. For instance, you can use pretender's [passthrough](https://github.com/pretenderjs/pretender#pass-through) feature to ignore data URLs: + +```javascript +import { getPretender } from 'ember-data-factory-guy'; + +// Passthrough 'data:' requests. +getPretender().get('data:*', getPretender().passthrough); +```