Git Product home page Git Product logo

ember-data-factory-guy's Introduction

Ember Data Factory Guy

Build Status Ember Observer Score npm version

Feel the thrill and enjoyment of testing when using Factories instead of Fixtures. Factories simplify the process of testing, making you more efficient and your tests more readable.

NEW starting with v3.8

  • jquery is no longer required and fetch adapter is used with ember-data
  • you can still use jquery if you want to
  • if you are addon author using factory guy set up your application adapter like this

NEW starting with v3.2.1

  • You can setup data AND links for your async relationship Check it out

NEW You can use factory guy in ember-twiddle

NEW If using new style of ember-qunit acceptance tests with setupApplicationTest check out demo here: user-view-test.js:

NEW starting with v2.13.27

  • get attributes for factory defined models with attributesFor

NEW starting with v2.13.24

  • manualSetup streamlined to manualSetup(this)

NEW and Improved starting with v2.13.22

Older but still fun things

Why is FactoryGuy so awesome

  • Since you're using ember data, you don't need to create any ORM like things
  • You don't need to add any files to recreate the relationships in your models
  • Any custom methods like: serialize / serializeAttribute / keyForAttribute etc... in a serializer will be used automatically
  • If you set up custom methods like: buildURL / urlForFindRecord in an adapter, they will be used automatically
  • You have no config file with tons of spew, because you declare all the mocks and make everything declaratively in the test
  • You can push models and their complex relationships directly to the store

Questions / Get in Touch

Visit the EmberJS Community #e-factory-guy Slack channel

Contents

How it works

  • You create factories for your models.
    • put them in the tests/factories directory
  • Use these factories to create models for your tests
    • you can make records that persist in the store
    • or you can build a json payload used for mocking an ajax call's payload

Installation

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:

  // 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

  • Brief sample of a factory definition:

  // 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
// 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)
  // 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:

  // 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
  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
  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
  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 )
  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

  let user = FactoryGuy.make('user', 'big', 'friendly', {name: 'Dave'});
  user.get('name') // => 'Dave'
  user.get('style') // => 'Friendly'
Using traits as functions
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:

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
  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

  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

  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.
  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:

  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

  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

  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
  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

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:

  // 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:

  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:

  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:

  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
  • nice way to get attibutes for a factory without making a model or payload
  • same arguments as make/build
  • no id is returned
  • no relationship info returned ( yet )
  import { attributesFor } from 'ember-data-factory-guy';

  // 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
  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

Usage:

  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
  • takes the same arguments as FactoryGuy.make
  • can compose relationships with other FactoryGuy.build/FactoryGuy.buildList payloads
  • can add relationship links to payload
  • takes serializer for model into consideration
  • to inspect the json use the get method
  • use the add method
    • to include extra sideloaded data to the payload
    • to include meta data
    • REMEMBER, all relationships will be automatically sideloaded, so you don't need to add them with the add() method

Usage:

  import { build, buildList } from 'ember-data-factory-guy';

  // 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
  let json = build('user', 'with_company', 'with_hats');
  json // =>
    {
      user: {
        id: 1,
        name: 'User1',
        company: 1,
        hats: [
          {type: 'big_hat', id:1},
          {type: 'big_hat', id:2}
        ]
      },
      companies: [
        {id: 1, name: 'Silly corp'}
      ],
      'big-hats': [
        {id: 1, type: "BigHat" },
        {id: 2, type: "BigHat" }
      ]
    }
FactoryGuy.buildList
  • for building json that you can pass as json payload in acceptance tests
  • takes the same arguments as FactoryGuy.makeList
  • can compose relationships with other build/buildList payloads
  • takes serializer for model into consideration
  • to inspect the json use the get() method
    • can use get(index) to get an individual item from the list
  • use the add method
    • to add extra sideloaded data to the payload => .add(payload)
    • to add meta data => .add({meta})

Usage:

  import { build, buildList } from 'ember-data-factory-guy';

  let bobs = buildList('bob', 2);  // 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:

  let batMan = build('bat_man');
  let userPayload = build('user').add(batMan);

  userPayload = {
    user: {
      id: 1,
      name: 'User1',
      style: "normal"
    },
    'super-heros': [
      {
        id: 1,
        name: "BatMan",
        type: "SuperHero"
      }
    ]
  };
  • when you want to add meta data to payload
    • only JSONAPI, and REST based and serializers and DRFSerializer can handle meta data
    • so JSONSerializer users can not use this feature ( though this might be a bug on my part )

Usage:

  let json1 = buildList('profile', 2).add({ meta: { previous: '/profiles?page=1', next: '/profiles?page=3' } });
  let json2 = buildList('profile', 2).add({ meta: { previous: '/profiles?page=2', next: '/profiles?page=4' } });

  mockQuery('profile', {page: 2}).returns({ json: json1 });
  mockQuery('profile', {page: 3}).returns({ json: json2 });

 store.query('profile', {page: 2}).then((records)=> // 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: to see 'boblike' and 'adminlike' user traits
  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
  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
  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.

      // 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
      // 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
        }
      }
      // 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. 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.

// 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:

// 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:

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:

Creating Factories in Addons

If you are making an addon with factories and you want the factories available to Ember apps using your addon, place the factories in test-support/factories instead of tests/factories. They should be available both within your addon and in Ember apps that use your addon.

Ember Django Adapter

  • available since 2.6.1
  • everything is setup automatically
  • sideloading is not supported in DRFSerializer so all relationships should either
    • be set as embedded with DS.EmbeddedRecordsMixin if you want to use build/buildList
    • or use make/makeList and in your mocks, and return models instead of json:
  let projects = makeList('projects', 2); // 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:

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)
      import { setupFactoryGuy } from "ember-data-factory-guy";
      
      module('Acceptance | User View', function(hooks) {
        setupApplicationTest(hooks);
        setupFactoryGuy(hooks);
      
        test("blah blah", async function(assert) {
           await visit('work');
           assert.ok('bah was spoken');
        });
      });
  • Sample model test: profile-test.js

    • Use moduleForModel ( ember-qunit ), or describeModel ( ember-mocha ) test helper
    • manually set up FactoryGuy
  • Sample component test: single-user-test.js

    • Using moduleForComponent ( ember-qunit ), or describeComponent ( ember-mocha ) helper
    • manually sets up FactoryGuy
    import { make, manualSetup }  from 'ember-data-factory-guy';
    import hbs from 'htmlbars-inline-precompile';
    import { test, moduleForComponent } from 'ember-qunit';
    
    moduleForComponent('single-user', 'Integration | Component | single-user (manual setup)', {
      integration: true,
    
      beforeEach: function () {
        manualSetup(this);
      }
    });
    
    test("shows user information", function () {
      let user = make('user', {name: 'Rob'});
    
      this.render(hbs`{{single-user user=user}}`);
      this.set('user', user);
    
      ok(this.$('.name').text().match(user.get('name')));
      ok(this.$('.funny-name').text().match(user.get('funnyName')));
    });

Acceptance Tests

  • For using new style of ember-qunit with setupApplicationTest check out demo here: user-view-test.js:
Using mock methods
  • Uses pretender

    • for mocking the ajax calls made by ember-data
    • pretender library is installed with FactoryGuy
  • http GET mocks

    • mockFindRecord
    • mockFindAll
    • mockReload
    • mockQuery
    • 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:
          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

  • Custom mocks (http GET/POST/PUT/DELETE)

  • Use method fails() to simulate failure

  • Use method succeeds() to simulate success

    • Only used if the mock was set to fail with fails() and you want to set the mock to succeed to simulate a successful retry
  • Use property timesCalled to verify how many times the ajax call was mocked

    • works when you are using mockQuery, mockQueryRecord, mockFindAll, mockReload, or mockUpdate
    • mockFindRecord will always be at most 1 since it will only make ajax call the first time, and then the store will use cache the second time
    • Example:
      const mock = mockQueryRecord('company', {}).returns({ json: build('company') });
    
      FactoryGuy.store.queryRecord('company', {}).then(()=> {
        FactoryGuy.store.queryRecord('company', {}).then(()=> {
          mock.timesCalled //=> 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:
      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:

  let errors401 = {errors: {description: "Unauthorized"}};
  let mock = mockFindAll('user').fails({status: 401, response: errors401});

  let errors422 = {errors: {name: "Name too short"}};
  let mock = mockFindRecord('profile').fails({status: 422, response: errors422});

  let errorsMine = {errors: [{detail: "Name too short", title: "I am short"}]};
  let mock = mockFindRecord('profile').fails({status: 422, response: errorsMine, convertErrors: false});
mockFindRecord
  • For dealing with finding one record of a model type => store.findRecord('modelType', id)
  • Can pass in arguments just like you would for make or build
    • mockFindRecord( fixture or model name, optional traits, optional attributes object)
  • Takes modifier method returns() for controlling the response payload
    • returns( model / json / id )
  • Takes modifier method adapterOptions() for setting adapterOptions ( get passed to urlForFindRecord )
  • Sample acceptance tests using mockFindRecord: user-view-test.js:

Usage:

   import { build, make, mockFindRecord } from 'ember-data-factory-guy';
  • To return default factory model type ( 'user' in this case )
   // 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
   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
   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
   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
   let user2 = build('user', {style: "boring"});
   mock.returns({ json: user2 });
   // mock.get('id') => 2
  • To mock failure case use fails method
   mockFindRecord('user').fails();
  • To mock failure when you have a model already
  let profile = make('profile');
  mockFindRecord(profile).fails();
  // mock.get('id') === profile.id
  • To use adapterOptions
  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
    • mockFindAll( fixture or model name, optional number, optional traits, optional attributes object)
  • Takes modifier method returns() for controlling the response payload
    • returns( models / json / ids )
  • Takes modifier method adapterOptions() for setting adapterOptions ( get passed to urlForFindAll )
    • used just as in mockFindRecord ( see example there )
  • Sample acceptance tests using mockFindAll: users-view-test.js

Usage:

   import { buildList, makeList, mockFindAll } from 'ember-data-factory-guy';
  • To mock and return no results
   let mock = mockFindAll('user');
  • Using returns({json}) to return json object
   // 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
    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
   let users2 = buildList('user', 3);
   mock.returns({ json: user2 });
  • To mock failure case use fails() method
   mockFindAll('user').fails();
mockReload
  • To handle reloading a model
    • Pass in a record ( or a typeName and id )

Usage:

  • Passing in a record / model instance
  let profile = make('profile');
  mockReload(profile);

  // will stub a call to reload that profile
  profile.reload()
  • Using returns({attrs}) to return new attributes
  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
  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
  mockReload('profile', 1).fails();
mockQuery
  • For dealing with querying for all records for a model type => store.query(modelType, params)
    • Takes modifier method returns() for controlling the response payload
      • returns( models / json / ids )
  • Takes modifier methods for matching the query params - withParams( object ) - withSomeParams( object )
  • Sample acceptance tests using mockQuery: user-search-test.js

Usage:

  import FactoryGuy, { make, build, buildList, mockQuery } from 'ember-data-factory-guy';
  let store = FactoryGuy.store;

  // 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 )
  // 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 )
  // 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 )
  // 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()
  // 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:

  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 )
  // 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 )
  // 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 )
  // 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.

  // 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:

  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
  // 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
      2. a function that can be used to perform an arbitrary match against the request json, returning true if there is a match, false otherwise.
    • returns
      • attributes ( including relationships ) to include in response json
  • Need to import run from @ember/runloop and wrap tests using mockUpdate with: run(function() { 'your test' })

Usage:

  import { make, mockUpdate } from 'ember-data-factory-guy';

  let profile = make('profile');

  // 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
  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

  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
  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
  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
  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
    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
  import { mock } from 'ember-data-factory-guy';

  this.mock({ url: '/users' });
  • Returning a JavaScript object
  import { mock } from 'ember-data-factory-guy';

  this.mock({
    type: 'POST',
    url: '/users/sign_in',
    responseText: { token: "0123456789-ab" }
  });

Pretender

The addon uses Pretender to mock the requests. It exposes the functions getPretender and setPretender to respectively get the Pretender server for the current test or set it. For instance, you can use pretender's passthrough feature to ignore data URLs:

import { getPretender } from 'ember-data-factory-guy';

// 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.
 let json    = buildList('widget', 'square', 'round', ['round','broken']);
 let widgets = makeList('widget', 'square', 'round', ['round','broken']);
 let [squareWidget, roundWidget, roundBrokenWidget] = widgets;
- you just built/made 3 different widgets from traits ('square', 'round', 'broken')
- the first will have the square trait
- the second will have the round trait
- the third will have both round and broken trait

Tip 2: Building static / fixture like data into the factories.

  • States are the classic case. There is a state model, and there are 50 US states.
  • You could use a strategy to get them with traits like this:
  import FactoryGuy from 'ember-data-factory-guy';

  FactoryGuy.define('state', {

    traits: {
      NY: { name: "New York", id: "NY" },
      NJ: { name: "New Jersey", id: "NJ" },
      CT: { name: "Connecticut", id: "CT" }
    }
  });

  // 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:
  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:

// 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

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
  // 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:
  let person = make('person', {name: "Daniel"});
  let json = person.serialize();
  assert.equal(json.name, 'Daniel-san');

ChangeLog

ember-data-factory-guy's People

Contributors

begedin avatar cristinawithout avatar danielspaniel avatar dcyriller avatar dependabot[bot] avatar dillonwelch avatar eliotpiering avatar indirect avatar juwara0 avatar krasnoukhov avatar lorcan avatar mblayman avatar mcm-ham avatar misterbyrne avatar mmelvin0 avatar mminkoff avatar opakalex avatar pangratz avatar patocallaghan avatar pedrokiefer avatar robdel12 avatar roschaefer avatar runspired avatar ryedeer avatar sduquej avatar subtletree avatar turbo87 avatar whatthewhat avatar wismer avatar xcambar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ember-data-factory-guy's Issues

HandleUpdate builds the wrong url

How handleUpdate builds the url for mocking doesn't seem to take into account the namespace preference supplied on an adapter.
If try mocking out an update from ember-data like this testHelper.handleUpdate('album', 1, {type: 'PUT'}) it builds a url for the model /albums/1' when it should really be/api/albums/1. I'm not really sure if this is an ember-data-factory-guy bug or something wrong with ember since it seems to be using the per adapterbuildUrl()` function which should work.

Also it seems that the default type for handleUpdate should be PUT since that's what ember-data uses by default when you do a save() on en existing model.

test 'update playCount', ->

  testHelper.handleUpdate('album', 1, {type: 'PUT'})
  # really gives testHelper.stubEndpointForHttpRequest('/albums/1', "{}", {type: 'PUT'})
  # should be testHelper.stubEndpointForHttpRequest('/api/albums/1', "{}", {type: 'PUT'})

  visit '/album/1'
  click 'button.play-album' # Generates a PUT to the server to update the album

  andThen ->
    equal find('#playCount').text(), "Played 1 times", "Updates the play count"
`import DS from 'ember-data'`

Album = DS.Model.extend
  name: DS.attr('string')
  artist: DS.attr('string')
  coverImage: DS.attr('string')
  year: DS.attr('date')
  playCount: DS.attr('number', {defaultValue: 0})
  tracks: DS.hasMany('track', { async: true })

`export default Album`
-------------------------------
`import DS from 'ember-data'`

AlbumAdapter = DS.RESTAdapter.extend
  namespace: 'api'

`export default AlbumAdapter`

hasMany associations

I would have expected the following to work for hasMany associations:

FactoryGuy.define('province');
FactoryGuy.define('country', {
  'default': {
    provinces: {}
  }
});

Checking the implementation it looks like this feature is simply not supported. Is that right?

Syntax to define sequences

Often a sequence function is used exactly once per factory (as the sequence is unique for an attribute), e.g. the sequence to generate a name of a customer. Those factories are not very DRY.

Example:

FactoryGuy.define('customer', {
  sequences: {
    name: function(num) { return "Customer" + num; },
    customerNumber: function(num) { return 1000 + num; }
  },
  default: {
    name: FactoryGuy.generate('name'),
    customerNumber: FactoryGuy.generate('customerNumber')
  }
});

Passing a function as a parameter to FactoryGuy.generate(...) would simplify the factories:
FactoryGuy.generate(function(num) { return "Customer" + num; })

Improvement: expecting specific parameters

While writing acceptance tests for my application, I have noticed that handleCreate, handleUpdate methods are pretty indifferent to what they receive.
For example we have code:

test 'create post', ->
  postData = FactoryGuy.build('post')
  factoryGuyHelper().handleCreate('post', postData)
  # fill form with data from "postData" and click on submit
  # assert page contains data from "postData" 

The problem is that we do not care what exactly has been sent to the server, we just accept anything and return post with postData.

What do you think about adding (or changing existing) methods to handle only MATCHED requests?

$.mockjax supports it by passing data parameter.

$.mockjax({
    url:  "/posts",
    data: { title: "foo" }
});

Possible API:

# will now respond only if post has title "foo"
factoryGuyHelper().handleCreate('post', { title: "foo" }) 

Dealing with multiple-inverse associations

Consider the following data model:

Location = DS.Model.extend
  name: DS.attr 'string'
  currentFloorplan: DS.belongsTo 'floorplan', async: true
  upcomingFloorplans: DS.hasMany 'floorplan', async: true

Floorplan = DS.Model.extend
  name: DS.attr 'string'
  location: DS.belongsTo 'location', async: true, inverse: null

A Floorplan always belongs to a Location, but the inverse association could be one of two different options, so we set it to null because Ember only allows a single inverse. Unfortunately there's no clean way to deal with this situation in Factory Guy, so we end up doing this Em.run workaround in every test that involves floorplans:

describe 'An association with multiple possible inverses', ->
  it 'requires a clunky workaround', ->
    location = factories.make 'location'
    floorplan = factories.make 'floorplan', location: location
    Em.run -> location.get('upcomingFloorplans').addObject(floorplan)

    expect(location.get('upcomingFloorplans.length')).to.equal 1

In Factory Girl, this kind of thing is usually handled with callbacks that access transient attributes to execute some custom behavior. It would of course be a substantial effort to add these features to Factory Guy, so maybe there is an easier targeted solution.

handleFindMany not correctly mocking requests?

I'm setting up a new Ember project for integration testing with Factory Guy, and couldn't get a very simple test to work. It went like this:

testHelper.handleFindMany('location', 1, name: 'DummyLoc')

visit '/'
click 'a[href="/locations"]' # This route loads all locations and displays them

andThen ->
  # Page body does not include 'DummyLoc'!

In the console I saw the mock being triggered, but no data was returned. Looking at the current code for handleFindMany, it looks like the mock request intentionally always returns nothing? This seems very odd. The mock did return actual data in the past, but that was changed in 63aee77. The tests don't catch this because they go directly to the data store rather than hitting the mocks.

For now I am using this workaround, since handleFindQuery does mock requests correctly:

location = testHelper.build('location', name: 'DummyLoc')
testHelper.handleFindQuery('location', null, [location])

visit '/'
click 'a[href="/locations"]' # This route loads all locations and displays them

andThen ->
  # Now the page body includes 'DummyLoc'

Avoid using store?

It's a PITA to get Ember's store Object while in tests. You have to do something like:

App.__container__.lookup('store:main')

So it would be nice if FactoryGuy offered some syntactic sugar. Instead of this:

store.makeFixture('user'); 

maybe FactoryGuy could offer something like this:

FactoryGuy.makeFixture('user'); 

or even just:

makeFixture('user'); 

Ember Data version constraint

Factory Guy currently locks Ember Data to 1.0.0-beta.12 exactly โ€“ current version is 1.0.0-beta.14.1. Any chance this constraint could be relaxed? I did a quick run of the Factory Guy test suite against the new version and had only one failure, something to do with the fixture adapter, though I'm not sure how to approach fixing that since I'm very new to Ember.

Use with Ember CLI

I recently came across this library, and am very interested in using it!

Are there any known issues when using it with Ember CLI? Any specific things I should know about integration?

Does anyone else have experience here?

Sequences

It would be nice to have FactoryGirl-style sequences.

In the meantime, I'm doing something like this:

FactoryGuy.define('user', {
  default: {
    name: 'Person 1',
    email: '[email protected]'
  },
  sequence: {
    name: function(n)  { return 'Person ' + n },
    email: function(n) { return 'person_' + n + '@example.com' }
  },
});


// makeFixtureList
//
// - assumes a factory set up like this:
//         FactoryGuy.define('user', {
//           default: {
//             name: 'Person',
//             email: '[email protected]'
//           },
//           sequence: {
//             name: function(n)  { return 'Person ' + n },
//             email: function(n) { return 'person_' + n + '@example.com' }
//           },
//         });
//     
//
//
// makeFixtureList(count, model, sequence_fixture_name = 'sequence')
// e.g.
//      makeFixtureList(10, 'user')   
//      - makes 10 users, using 'sequence' as the name generators, and 'default' to actually create the users
//      - the equvalent of calling:
//            store.makeFixture('user', { name: 'Person 1', email: '[email protected]' })
//            store.makeFixture('user', { name: 'Person 2', email: '[email protected]' })
//            ...
//            store.makeFixture('user', { name: 'Person 10', email: '[email protected]' })
//
var makeFixtureList = function(count, model, sequence_fixture_name) {
  var store = App.__container__.lookup('store:main'));

  if (!sequence_fixture_name) sequence_fixture_name = 'sequence';

  var attributes_generators = FactoryGuy.getModelInfo(model)[sequence_fixture_name];

  for (var i = 1; i <= count; i++) {
    var attributes = {}; 
    for (var key in attributes_generators) {
      var func = attributes_generators[key];
        var text = func(i);
        attributes[key] = text;
    }

    store.makeFixture(model, attributes);
  }

}

Our lovely "'typeKey' of undefined" again

http://jsbin.com/qihin/1/edit?js,output

I'm using beta11 of ember-data.
Here are my models:

App.Progress = DS.Model
App.Unit = DS.Model.extend
  lesson: DS.belongsTo 'lesson', async: true

App.Lesson = DS.Model.extend
  steps: DS.hasMany 'step', async: true
  progress: DS.belongsTo 'progress', { async: true, inverse: null }

App.Step = DS.Model.extend
  progress: DS.belongsTo 'progress', { async: true, inverse: null }

As you see step and lesson both has progress. And I use inverse: null to indicate that I don't need other side. It's in public API actually -- http://emberjs.com/guides/models/defining-models/#toc_reflexive-relation

And here are my fabrics:

FactoryGuy.define 'progress',
  default: {}
FactoryGuy.define 'step',
  default: 
    progress: FactoryGuy.belongsTo 'progress'
FactoryGuy.define 'lesson',
  default:
    steps: FactoryGuy.hasMany 'step', 2
    progress: FactoryGuy.belongsTo 'progress'
FactoryGuy.define 'unit',
  default:
    lesson: FactoryGuy.belongsTo('lesson')

Trying to create unit leads to the same error:

TypeError: Cannot read property 'typeKey' of undefined
    at Ember.Object.extend.modelFor (http://builds.emberjs.com/tags/v1.0.0-beta.11/ember-data.js:11256:22)
    at Ember.Object.extend.recordForId (http://builds.emberjs.com/tags/v1.0.0-beta.11/ember-data.js:10727:25)
    at deserializeRecordId (http://builds.emberjs.com/tags/v1.0.0-beta.11/ember-data.js:11692:27)
    at http://builds.emberjs.com/tags/v1.0.0-beta.11/ember-data.js:11670:11
    at http://builds.emberjs.com/tags/v1.0.0-beta.11/ember-data.js:9456:20
    at http://builds.emberjs.com/tags/v1.0.0-beta.11/ember-data.js:5295:33
    at http://builds.emberjs.com/tags/v1.7.0/ember.js:14897:20
    at Object.OrderedSet.forEach (http://builds.emberjs.com/tags/v1.7.0/ember.js:14739:14)
    at Object.Map.forEach [as __super$forEach] (http://builds.emberjs.com/tags/v1.7.0/ember.js:14895:14)
    at Object.mapForEach [as forEach] (http://builds.emberjs.com/tags/v1.0.0-beta.11/ember-data.js:5302:14)

allow for afterMake callbacks in named definitions

Currently you can use afterMake on a model definition .. which will apply globally to all models of that type that are made through FactoryGuy.make. Would be nice to have afterMake also to be declared in named definitions which would take precedence or perhaps override? global afterMake ( if it exists )

resetModels when usingFixtureAdapter?

We override the useFixtureAdapter call in FactoryGuyTestMixin because it conveniently sets all fixtures to an empty array. If we don't call it, we get these errors:

Error: Error: Assertion Failed: Unable to find fixtures for model type App.ModelType)

We override the FactoryGuyTestMixin like this to fix the problem:

Ember.lookup.TestHelper = (Ember.Object.extend FactoryGuyTestMixin,
  useFixtureAdapter: ->
    @_super.apply(@, arguments)
    FactoryGuy.resetModels(@getStore())
).create()

Is there any downside to having FactoryGuy do resetModels when calling useFixtureAdapter? I'm happy to do the PR if this behavior is acceptable.

Compound factory specification

Is it possible to combine factory traits akin to FactoryGirl's implementation?

e.g. FactoryGuy.build('user', traits: ['admin', 'funny_name', 'unconfirmed'])

Issue when using handleUpdate

I'm writing some tests using handleUpdate, but it keeps failing. I think it's because factory-guy returns an empty set on PUT requests.

The error message is the following:
"An adapter cannot assign a new id to a record that already has an id. <my-app@model:mymodel::ember882:1> had id: 1 and you tried to update it with null. This likely happened because your server returned data in response to a find or update that had a different id than the one you sent."

In this project I'm using ember-data-tastypie-adapter, ember 1.9.1 and ember-data beta.14. Maybe returning the id would solve, or serializing the hole object, not sure though. Maybe this specific for this adapter, or related to beta.14.

Multiple calls to handleCreate() don't work

I have a test file with multiple test() calls. Inside two I have calls to handleCreate(). Each test run individually succeeds, but together they fail since only the first call to handleCreate() "takes" or "sticks".

https://gist.github.com/pixelterra/cd4e6d7ac3e4794bdf58

This is pretty surprising behavior to me. I would assume that only the "last" call to handleCreate() would be "active".

I suppose that I could work around this by using the match() method and tracking which data is used in each call, but this seems unnecssary and could get messy with many calls to handleCreate().

Is this the only option or am I missing something?

Is not finding factories that are in tests/factories/

I am having to manually include any factory file that I want to use. I can see the there is some kind of initializer in the source that is supposed to load them, but this doesn't seem to run. It ends up being commented out in my vendor.js

Unloading records in teardown causes intermittent failure

The new documentation in the README includes this part about acceptance testing:

  teardown: function () {
    Ember.run(function () {
      TestHelper.teardown();
      App.destroy();
    });
  }

As part of the TestHelper.teardown call, Factory Guy unloads all records from the store. The problem is that the app is still running at this point, and App.destroy is asynchronous and does not destroy the app immediately. Unloading the records can therefore raise errors (e.g. in computed properties that depend on associations) and cause tests to fail.

The thing is, App.destroy seems to already unload all records from the store. I tested this by commenting out the teardown line, and observing that the count of local records was reset to 0 by the time each new test started. I am not using the fixture adapter so maybe things would be different there, but in my case I was able to replace TestHelper.teardown() with a simple $.mockjax.clear() (since that is the other thing the teardown call does), and that resolved the intermittent failures.

I'm not sure if this means the record unloading could safely be removed for everyone, just noting that it caused a problem for me and doesn't seem to be strictly necessary.

Install is not working

$ ember install:addon ember-data-factory-guy
version: 0.2.1

A new version of ember-cli is available (0.2.3). To install it, type ember update.
Could not find watchman, falling back to NodeWatcher for file system events.
Visit http://www.ember-cli.com/#watchman for more info.
Installed packages for tooling via npm.
Cannot read property 'name' of undefined
TypeError: Cannot read property 'name' of undefined
at /home/blieb/projects/akita/frontend/node_modules/ember-cli/lib/models/project.js:459:53
at find (/home/blieb/projects/akita/frontend/node_modules/ember-cli/node_modules/lodash-node/modern/collections/find.js:64:11)
at Project.findAddonByName (/home/blieb/projects/akita/frontend/node_modules/ember-cli/lib/models/project.js:458:20)
at Class.module.exports.Task.extend.findDefaultBlueprintName (/home/blieb/projects/akita/frontend/node_modules/ember-cli/lib/tasks/addon-install.js:63:30)
at Class.module.exports.Task.extend.installBlueprint (/home/blieb/projects/akita/frontend/node_modules/ember-cli/lib/tasks/addon-install.js:52:30)
at Class. (/home/blieb/projects/akita/frontend/node_modules/ember-cli/lib/tasks/addon-install.js:42:19)
at lib$rsvp$$internal$$tryCatch (/home/blieb/projects/akita/frontend/node_modules/ember-cli/node_modules/rsvp/dist/rsvp.js:489:16)
at lib$rsvp$$internal$$invokeCallback (/home/blieb/projects/akita/frontend/node_modules/ember-cli/node_modules/rsvp/dist/rsvp.js:501:17)
at lib$rsvp$$internal$$publish (/home/blieb/projects/akita/frontend/node_modules/ember-cli/node_modules/rsvp/dist/rsvp.js:472:11)
at lib$rsvp$asap$$flush (/home/blieb/projects/akita/frontend/node_modules/ember-cli/node_modules/rsvp/dist/rsvp.js:1290:9)

{
"name": "akita",
"version": "0.1.0-SNAPSHOT",
"description": "Front end for user namagement through NED",
"private": true,
"directories": {
"doc": "doc",
"test": "tests"
},
"scripts": {
"start": "ember server",
"build": "ember build",
"test": "ember test"
},
"repository": "",
"engines": {
"node": ">= 0.10.0"
},
"author": "",
"license": "MIT",
"devDependencies": {
"bower": "^1.3.12",
"broccoli-asset-rev": "^2.0.0",
"broccoli-static-compiler": "0.2.1",
"ember-cli": "0.2.1",
"ember-cli-bootstrap-sass": "^0.2.11",
"ember-cli-content-security-policy": "0.3.0",
"ember-cli-dependency-checker": "0.0.8",
"ember-cli-document-title": "0.0.1",
"ember-cli-esnext": "0.1.1",
"ember-cli-htmlbars": "0.7.4",
"ember-cli-ic-ajax": "0.1.1",
"ember-cli-inject-live-reload": "^1.3.0",
"ember-cli-qunit": "0.3.9",
"ember-cli-sass": "^3.1.0-beta.5",
"ember-data": "git+https://github.com/emberjs/data#4c112b04bc14fa389cfa9c21420bec7e36c717da",
"ember-data-factory-guy": "1.0.2",
"ember-export-application-global": "^1.0.0",
"ember-validations": "^2.0.0-alpha.3",
"express": "^4.8.5",
"glob": "^4.0.5"
},
"dependencies": {
"ember-cli-app-version": "^0.3.3"
}
}

add .andFail() option to handleFind()

I'm currently writing a integration spec for a ember-rails project where I want to test if our notfound a.k.a 404 route is triggered when a resource is requested that does not exist.

this is what I have so far:

test('renders not-found page for nonexisting ids', function () {
  expect(1);

  visit('/products/1');

  testHelper.handleFind('product', {id: "1"});

  andThen(function () {
    expectRouteToBe('not_found'); //custom helper that checks if currentPath() equals X
  });
});

The problem is that I do want to mock the /products/1 endpoint BUT i want it to fail and return a 404. As far as I'm aware off this functionality isn't implemented as of this moment (but I could be wrong).

When I look at the API documentation I see that there is a .andFail option for handleCreate() and handleUpdate() it would be nice to have this option for the handleFind() helper as well.

I'm willing to create a PR for this (including specs), as long as you are interested in this functionality / use-case.

Match only set attributes

Hello!

Do you think we need match only attributes that are not null?

Current code for handleCreate is:

    if (opts.match) {
      var expectedRequest = {};
      var record = this.getStore().createRecord(modelName, match);
      expectedRequest[modelName] = record.serialize();
      httpOptions.data = JSON.stringify(expectedRequest);
    }

I believe it will not work if model is Post(title: 'Some title', body: 'Some body'), handleCreate is match: {title: 'Some title'} (let's say I don't care about body field here), it will match to title: 'Some title', body: null so my request will not be handled.

This will fix the problem:

    if (opts.match) {
      var expectedRequest = {};
      var record = this.getStore().createRecord(modelName, match);
      expectedRequest[modelName] = record.serialize();
      filteredExpectedRequest = {};
      for(field in expectedRequest) { 
           if(expectedRequest[field] != null) {
                filteredExpectedRequest[field] = expectedRequest[field]; 
           }
      }
      httpOptions.data = JSON.stringify(filteredExpectedRequest);
    }

AMD/Require and use strict

Hey,
I really am quite interested in using this library for dev/testing however in its current state I am unable to use it.

Issues:

  1. I use 'use strict' within my build process which rejects undeclared (var) variables. var is not used when defining classes within this library.
  2. Unless I am missing something obvious, there is no support for AMD/Require JS

Question:

  1. I am more than willing to fork, change and submit a patch to resolve the above - is this something you would be willing to accept into your library?

If you are totally against strict and AMD then there is no point me spending time forking and patching.

Thanks

Davie

id not included in toJSON()

The examples show the id being included in the return value of toJSON() like this:

// returns a User instance that is loaded into your application's store   
var user = store.makeFixture('user');
user.toJSON() // => {id: 2, name: 'Dude', style: 'normal'}

However, ember data's json serializer doesn't return the id unless {includeId: true} is used like this:

// returns a User instance that is loaded into your application's store   
var user = store.makeFixture('user');
user.toJSON({includeId: true}) // => {id: 2, name: 'Dude', style: 'normal'}

So, maybe the README should be changed (I can submit a pull request), or maybe I'm just doing something wrong. Please advise.

Passing match to handleCreate results in false positive

First off, thanks for writing this library. I think it will drastically clean up my tests.

I'm using handleCreate to replace my existing sinon mock. After reading through the docs, I saw the match option. This seems like a really cool feature, but I keep getting false positives when trying to use it.

For example:

TestHelper.handleCreate('project', {match: {name: "Not Tony"}});

I then proceed to write my test code to create an author with the name of "Tony". I would expect that to fail based on the match condition.

Upon closer inspection I saw the code inside MockCreateRequest's calculate method is calling tmpRecord.serialize(matchArgs). Looking at the ember data source, I see this for serialize:

  /*
    Create a JSON representation of the record, using the serialization
    strategy of the store's adapter.

   `serialize` takes an optional hash as a parameter, currently
    supported options are:

   - `includeId`: `true` if the record's ID should be included in the
      JSON representation.

    @method serialize
    @param {Object} options
    @return {Object} an object whose values are primitive JSON values only
  */
  serialize: function(options) {
    return this.store.serialize(this, options);
  },

So, it doesn't seem like the match args are actually going to survive beyond the call to serialize. That would explain my false positive. Am I interpreting your README correctly?

Thanks again :)

ember-data beta 16.1
ember 1.11.3
ember-data-factory-guy 1.0.7
ember-cli 0.2.3

Does not use model-specific adapters for buildURL

As described at http://emberjs.com/guides/models/customizing-adapters/, each model can have its own adapter where you can customize the method pathForType.

Ember Data Factory Guy uses the default adapter during buildURL, so customizations in model-specific adapters in the pathForType method are not used when building the stub endpoint URL.

This may also be what's happening in #29, but I'm not certain of that so creating separate issue. Pull request to follow.

"typeKey of undefined" #3

Hi again!
JSbin here
Having this models

App.Unit = DS.Model.extend
  lesson: DS.belongsTo 'lesson', async: true
App.Lesson = DS.Model.extend
  steps: DS.hasMany 'step', async: true
App.Step = DS.Model.extend 
  lesson: DS.belongsTo 'lesson', async: true

And this fabrics:

FactoryGuy.define 'unit',
  default:
    lesson: FactoryGuy.belongsTo('lesson')
FactoryGuy.define 'lesson',
  default:
    steps: FactoryGuy.hasMany('step', 2)
FactoryGuy.define 'step',
  default: {}
```coffeescript
I'm trying to create unit:

viewHelper.make 'unit'

But it's failing with this stacktrace:
stack trace:
```javascript

TypeError: Cannot read property 'typeKey' of undefined
    at Ember.Object.extend.modelFor (http://builds.emberjs.com/canary/ember-data.js:11258:22)
    at Ember.Object.extend.recordForId (http://builds.emberjs.com/canary/ember-data.js:10729:25)
    at deserializeRecordId (http://builds.emberjs.com/canary/ember-data.js:11694:27)
    at http://builds.emberjs.com/canary/ember-data.js:11672:11
    at http://builds.emberjs.com/canary/ember-data.js:9458:20
    at http://builds.emberjs.com/canary/ember-data.js:5295:33
    at http://builds.emberjs.com/tags/v1.7.0/ember.js:14897:20
    at Object.OrderedSet.forEach (http://builds.emberjs.com/tags/v1.7.0/ember.js:14739:14)
    at Object.Map.forEach [as __super$forEach] (http://builds.emberjs.com/tags/v1.7.0/ember.js:14895:14)
    at Object.mapForEach [as forEach] (http://builds.emberjs.com/canary/ember-data.js:5302:14)

payload of handleFind limited to record's typeKey

The API I'm working with uses single table inheritance. For example, we have a base task model, and various subclasses.

In my test I'm trying to do a handleFind(task). That task happens to be of type paper-editor-task. The factory guy method mapFind will always return a json payload keyed to the pluralized version of the model name resulting in paperEditorTasks. My ember app expects tasks.

As a workaround I've monkey patched the FactoryGuy code. Would you consider supporting this? Perhaps as an option to handleFind? Or maybe by looking into the model's serializer instead of always using record.constructor.typeKey.

Manually set handleCreate response.

Is it possible to set the status code and response text for 'handleCreate'? I try to mock a create request that should produce a validation error on server side, therefor my response should be something like:

status: 422
respsonseText:

{
   errors: {
      attr1: [error1, error2, ...],
      attr2: [error1, error2, ...]
    }
}

Integration tests bypassing store and making server requests.

I'm writing integration style tests for my embercli project and have come across an issue where I think I've stubbed correctly but still see network requests.

This is my integration spec which visits a particular route that loads all the album models so I've created a bunch of albums via store.makeList('album', 5) which I was expecting to prevent any real ajax calls being made.

Despite this setup I still see GET http://localhost:4200/api/albums 404 (Not Found) when I run the embercli tests in a browser. I've tried using the handleFind function but that seems targeted towards an individual model and doesn't work either.

Am I missing part of the API that covers stubbing out things like @store.find('albums') which loads all albums? It seems like those calls bypass anything in the store and always try to make an ajax call.

Any help would be appreciated. Other than this issue I'm really liking this library, seems to do most of the things I want and makes it very easy. Cheers.

`import Ember from 'ember'`
`import startApp from '../helpers/start-app'`
`import albumFactory from '../fixtures'`

App = null
testHelper = null
store = null
TestHelper = Ember.Object.createWithMixins(FactoryGuyTestMixin)

module 'Acceptance: Dashboard',
  setup: ->
    App = startApp()
    testHelper = TestHelper.setup(App)
    store = testHelper.getStore()
    albums = store.makeList('album', 5)

  teardown: ->
    Ember.run -> testHelper.teardown()
    Ember.run App, 'destroy'

test 'visiting /dashboard', ->
  visit '/dashboard'

  andThen ->
    equal currentPath(), 'dashboard'

test 'displays top albums', ->
  visit '/dashboard'

  andThen ->
    equal(find('.top-albums li').length, 5, "Displays the top 5 albums")

My model definition

`import DS from 'ember-data'`

Album = DS.Model.extend
  name: DS.attr('string')
  artist: DS.attr('string')
  coverImage: DS.attr('string')
  year: DS.attr('date')
  playCount: DS.attr('number', {defaultValue: 0})
  tracks: DS.hasMany('track', { async: true })

`export default Album`

and the fixtures file.

FactoryGuy.define('album', {
  sequences: {
    albumName: (num) ->
      'Album' + num

    artistName: (num) ->
      'Artist' + num
  },
  default: {
    name: FactoryGuy.generate('albumName'),
    artist: FactoryGuy.generate('artistName')
  }
});

`export default {}`

handleCreate seems to require an Em.run

When using testHelper.handleCreate in an acceptance test, I get an error that requires me to wrap it in an Em.run call. It looks like this is because handleCreate uses createRecord, which can have asynchronous side effects.

fix versioning

@danielspaniel could you please remove tags v4.0 and v4.1. It leads to some troubles when using bower or other PM, they all treat v4.1 as the latest
image

Using store.find, unable to find fixtures for model type (subclass of DS.Model). If you're defining your fixtures using `Model.FIXTURES = ...`, please change it to `Model.reopenClass({ FIXTURES: ... })`.

i am using active model adapter and my test looks like this

import Ember from 'ember';
import startApp from '../../helpers/start-app';
import { moduleForModel, test } from 'ember-qunit';
import FactoryGuy, { make, build } from 'ember-data-factory-guy';

var application;

moduleForModel('user-notification', 'User Notification Model', {
   needs: ['model:patient', 'model:payment', 'model:user'],
   beforeEach: function() {
    application = startApp();
   },
   afterEach: function() {
    Ember.run(application, 'destroy');
   }
});

test('practice_manager_payment_succeeded notification works correctly', function(assert) {
  var store = this.store();
  var user = make('user');
  var patient = make('patient');
  var payment = make('payment');
  var messageJson = { user: user.get('id'), payment: payment.get('id'), patient: patient.get('id') };
  messageJson = JSON.stringify(messageJson);
  var userNotification = build('user-notification', {eventType: "practice_manager_payment_succeeded", messageJson: messageJson});
  var model = this.subject(userNotification);
  assert.strictEqual(model.get('message'), "Tommy Ton took a $20.00 payment from Richard Hendricks", 'User notification message is Tommy Ton took a $20.00 payment from Richard Hendricks');
});

and in my ember data user notification model, i have a computed property that gets triggered, the relevant bit of code is here

switch(this.get('eventType')) {
  case "practice_manager_payment_succeeded":
    debugger;
    promiseHash = Ember.RSVP.hash({
                                   user: self.store.find('user', messageJsonObject.user),
                                   payment: self.store.find('payment', messageJsonObject.payment),
                                   patient: self.store.find('patient', messageJsonObject.patient)
                             });
    break;
}

the self.store.find throws

Unable to find fixtures for model type (subclass of DS.Model). If you're defining your fixtures using `Model.FIXTURES = ...`, please change it to `Model.reopenClass({ FIXTURES: ... })`.

checking the store at this point (debugger line above) shows me the relevant records that were set up using the factory guy make are available, is there any to get around this?

your example model test shows you don't use moduleForModel, is this intentional?

https://github.com/danielspaniel/ember-data-factory-guy/blob/master/tests/unit/models/user-test.js

versions

ember 1.9.1
ember-data 1.0.0-beta.16.1
ember-cli 0.2.3

Release incorporating `var` fix?

Any chance we could get a patch release that incorporates 27ea2fc? The missing var causes an error in strict mode, preventing us from running our Ember-CLI tests.

Any way to configure a api namespace for FactoryGuyTestMixin?

I've added a namepace to my RESTAdapter:

DS.RESTAdapter.reopen({
  namespace: 'api/v1'
});

FactoryGuyTestMixin request handlers ( e.g. FactoryGuyTestMixin#handleFind ) construct a url that's inferred from the name of the factory:
https://github.com/danielspaniel/ember-data-factory-guy/blob/c22a7c4bf459ba8c49e1be15afc520d66d95992b/src/factory_guy_test_mixin.js#L127

Would it be reasonable to add a configuration setting to FactoryGuyTestMixin for a base url and then prepend that to the url while building the mockjax request here?:
https://github.com/danielspaniel/ember-data-factory-guy/blob/c22a7c4bf459ba8c49e1be15afc520d66d95992b/src/factory_guy_test_mixin.js#L69

FactoryGuy.hasMany fails with error `'undefined' is not an object (evaluating 'traits[trait]')`

I'm running into this issue with trying to test a model that has an association on it. I found a temporary solution but I've yet to understand the root cause or how to test it (I'm new to ember). Anyway, here is what I hope is all of the relevant info

The models

// app/group/model.js
import DS from 'ember-data';

export default DS.Model.extend({
  name: DS.attr('string'),

  users: DS.hasMany('user')
});

// app/user/model.js
import DS from 'ember-data';

export default DS.Model.extend({
  name: DS.attr('string'),
  email: DS.attr('string'),
  role: DS.attr('string'),
  last_signed_in_at: DS.attr('datetime')
});

The factories

// tests/factories/group.js
import FactoryGuy from 'ember-data-factory-guy';
import faker from 'faker';

FactoryGuy.define('group', {
  default: {
    name: faker.company.companyName()
  },

  traits: {
    with_users: { users: FactoryGuy.hasMany('user', 4) }
  }
});

// tests/factories/user.js
import FactoryGuy from 'ember-data-factory-guy';
import faker from 'faker';

FactoryGuy.define('user', {
  default: {
    name: faker.name.findName(),
    email: faker.internet.email(),
    role: 'member',
    last_signed_in_at: Date.now()
  },

  admin_user: {
    role: 'admin'
  }
});

The test

// tests/unit/group/model-test.js
import Ember from 'ember';
import startApp from 'client/tests/helpers/start-app';
import { module, test } from 'qunit';

import { make } from 'ember-data-factory-guy';

var application;

module('Group model', {
  beforeEach: function() {
    application = startApp();
  },

  afterEach: function() {
    Ember.run(application, 'destroy');
  }
});

test('it exists', function(assert) {
  let group = make('group');
  assert.ok(!!group);
  assert.ok(group.get('name'));
});

test('it can have user', function(assert){
  let group = make('group','with_users');

  assert.equal(group.get('users.length'), 4, '4 of them');
});

The error

not ok 3 PhantomJS 1.9 - Group model: it can have user
    ---
        actual: >
            null
        message: >
            Died on test #1     at http://localhost:7357/assets/test-support.js:2809
                at http://localhost:7357/assets/client.js:3188
                at http://localhost:7357/assets/vendor.js:150
                at tryFinally (http://localhost:7357/assets/vendor.js:30)
                at http://localhost:7357/assets/vendor.js:156
                at http://localhost:7357/assets/test-loader.js:29
                at http://localhost:7357/assets/test-loader.js:21
                at http://localhost:7357/assets/test-loader.js:40
                at http://localhost:7357/assets/test-support.js:5578: 'undefined' is not an object (evaluating 'traits[trait]')
        Log: >
    ...

What causing the error is the association line in my group factory trait for users. If I remove the hasMany, I receive no error.

traits: {
 with_users: { users: FactoryGuy.hasMany('user', 4) }
}

What fixes it

So what's bombing out there is model-definition.js

  this.build = function (name, opts, traitArgs) {
    var traitsObj = {};
    traitArgs.forEach(function (trait) {
      $.extend(traitsObj, traits[trait]);
    });
...

For some reason, the extractArguments method is actually setting traits = undefined which is then causing that traitArgs argument in build() to equal an array with a single value of undefined ([undefined])

Now, I have no idea if that is desired or not, i've only dug around trying to get this to work for an hour or two. What got things to work for me was putting in a guard clause.

traitArgs.forEach(function (trait) {
  if (!trait) return;
  $.extend(traitsObj, traits[trait]);
});

If that really does fix it, great. Otherwise, I hope all of this rambling is enough to point you in the right direction of fixing the bug or telling me what obvious thing I missed. Thanks!

Versions

DEBUG: -------------------------------
ember.debug.js:5197DEBUG: Ember                     : 1.11.1
ember.debug.js:5197DEBUG: Ember Data                : 1.0.0-beta.16.1
ember.debug.js:5197DEBUG: jQuery                    : 1.11.3
ember.debug.js:5197DEBUG: Ember Simple Auth         : 0.8.0-beta.2
ember.debug.js:5197 DEBUG: Ember Simple Auth Devise  : 0.8.0-beta.2
ember.debug.js:5197 DEBUG: Ember Simple Auth Testing : 0.8.0-beta.2
ember.debug.js:5197 DEBUG: -------------------------------
    "ember-data-factory-guy": "1.0.5",

.reload()

Calling .reload() causes weird request: GET http://localhost:7357/companies/1/tests/index.html 404 (Not Found).

Cannot read property 'typeKey' of undefined

Have no idea what am I doing wrong here is well prepared jsbin โ€“ http://jsbin.com/wamana/12/edit

Having following models:

App.Lesson = DS.Model.extend
  title: DS.attr 'string'
App.Unit = DS.Model.extend
  lesson: DS.belongsTo 'lesson', async: true

And following fabrics:

FactoryGuy.define 'lesson',
  default:
    title: "Lesson"
FactoryGuy.define 'unit',
  default:
    lesson: FactoryGuy.belongsTo('lesson')

I'm trying to create unit:

viewHelper.make 'unit'

viewHelper it's and object created as described in docs โ€“ https://github.com/danielspaniel/ember-data-factory-guy#integration-tests

stack trace:

TypeError: Cannot read property 'typeKey' of undefined
    at Ember.Object.extend.modelFor (http://builds.emberjs.com/canary/ember-data.js:11258:22)
    at Ember.Object.extend.recordForId (http://builds.emberjs.com/canary/ember-data.js:10729:25)
    at deserializeRecordId (http://builds.emberjs.com/canary/ember-data.js:11694:27)
    at http://builds.emberjs.com/canary/ember-data.js:11672:11
    at http://builds.emberjs.com/canary/ember-data.js:9458:20
    at http://builds.emberjs.com/canary/ember-data.js:5295:33
    at http://builds.emberjs.com/tags/v1.7.0/ember.js:14897:20
    at Object.OrderedSet.forEach (http://builds.emberjs.com/tags/v1.7.0/ember.js:14739:14)
    at Object.Map.forEach [as __super$forEach] (http://builds.emberjs.com/tags/v1.7.0/ember.js:14895:14)
    at Object.mapForEach [as forEach] (http://builds.emberjs.com/canary/ember-data.js:5302:14)

ES6 & Broccoli

What do you think about migrating to ES6 and using Broccoli as the build chain? As that seems to be the default for all ember related projects.

If that is something you would like to have on the project I can work on a PR for that.

License file

I want to use Factory Guy in a project, but I'd like it to have a license file first.

Looking at the package.json and bower.json files I can see the license is MIT, but I'd also like it to have a license file to reflect this.

Could someone add one?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.