Skip to content

Latest commit

 

History

History
269 lines (220 loc) · 9.63 KB

tutorial.md

File metadata and controls

269 lines (220 loc) · 9.63 KB

Tutorial

This tutorial introduces the capabilities of factory-girl. We'll start with a simple factory for a hypothetical User model and gradually add to it. This tutorial may not cover all aspects of factory-girl, but should serve as a good starting point.

The User Factory

Let's start with a simple User factory, as we go on, we'll keep on modifying this factory to add functionality and show how factory-girl works.

import factory from 'factory-girl';
import User from '../models/User';

factory.define('User', User, {
  email: 'user@my-domain.com',
  password: 'some-password'
});

factory.build('User').then(user => {
  console.log(user);
  // => User { email: 'user@my-domain.com', password: 'some-password' }
});

Whenever we need a User object now, we can just ask factory-girl to build one for us. That's awesome, but not very useful yet. All the objects we get back from factory-girl currently have same property values.

The #build api allows us to pass attributes to override default ones, so we can do:

factory.build('User').then(user => console.log(user));
// => User { email: 'user@my-domain.com', password: 'some-password' }
factory.build('User', {email: 'another-user@my-domain.com'}).then(user => console.log(user));
// => User { email: 'another-user@my-domain.com', password: 'some-password' }

Once again, this may be handy, but not very useful, it requires us to keep providing a new email value each time we want to create a User model.

factory-girl has a solution: sequences. Instead of providing a hardcoded value, we can tell factory-girl to instead use a sequence. A slight modification to the model-factory definition:

factory.define('User', User, {
  email: factory.sequence('User.email', n => `dummy-user-${n}@my-domain.com`),
  password: 'some-password'
});

Now we get a new email address every time we ask factory-girl for a User instance:

factory.build('User').then(user => console.log(user));
// => User { email: 'dummy-user-1@my-domain.com', password: 'some-password' }
factory.build('User').then(user => console.log(user));
// => User { email: 'dummy-user-2@my-domain.com', password: 'some-password' }

Better! Let's add name and about attributes for our User models:

factory.define('User', User, {
  email: factory.sequence('User.email', n => `dummy-user-${n}@my-domain.com`),
  password: 'some-password'
  name: factory.sequence('User.name', n => `user name ${n}`),
  about: 'this ideally should be a paragraph about user',
});

This should work fine, but what if you have a few test cases that expect about to be actually a paragraph? Or rather have user names that look a bit realistic instead of user name 1? factory-girl provides another goodie that we can use for a more 'realistic' data: chancejs. You can learn more about chancejs here. factory-girl exposes a simple '#chance' api that can be easily used to populate fields with data generated by chancejs.

factory.define('User', User, {
  email: factory.sequence('user.email', n => `dummy-user-${n}@my-domain.com`),
  password: factory.chance('word'),
  name: factory.chance('name'),
  about: factory.chance('paragraph'),
});

What if you want about to have just 2 sentences or names to have a middle name as well or passwords to be a bit longer? No problem, you can just pass any options expected by the chancejs api:

factory.define('User', User, {
  email: factory.sequence('user.email', n => `dummy-user-${n}@my-domain.com`),
  password: factory.chance('word', { syllables: 4 }),
  name: factory.chance('name', { middle: true }),
  about: factory.chance('paragraph', { sentences: 2 })
});

Our User factory will now create instances such as:

User {
  email: 'dummy-user-1@my-domain.com',
  password: 'tavnamgi',
  name: 'Nelgatwu Powuku Heup',
  about: 'Idefeulo foc omoemowa wahteze liv juvde puguprof epehuji upuga zige odfe igo sit pilamhul oto ukurecef.'
}

Let's say we want our User instances to have a password expiry date. Assuming the date needs to be in future (apart from the test case where it shouldn't), hard-coding the date doesn't seems elegant. Let's say by default we want the expiry date to be a month from now (i.e. when the instance is being created).

Anywhere you need to do something to compute a value for an attribute, you can provide a function that returns the value. Our User factory now becomes:

factory.define('User', User, {
  ...
  passwordExpiry: () => moment().add('1 month').toDate(),
});

What if you want to do something asynchronous? Anywhere you want to do something asynchronous, just provide a function that returns a promise that resolves to the value to be populated.

factory.define('User', User, {
  ...
  favoriteJoke: () => fetch('http://api.icndb.com/jokes/random')
      .then(res => res.json()).then(data => data.joke)
});

So far we have been dealing with a single model. Most of the times you have several models associated with each other there are a few ways factory-girl allows you to have associations. Let's say we would like to have our users to have a profile image. We start by defining a factory for profile image model (assuming we already have a ProfileImage model):

factory.define('ProfileImage', ProfileImage, {
  id: factory.sequence('ProfileImage.id'),
  imageUrl: 'http://lorempixel.com/200/200'
});

To associate a profile image with factory generated models, we can simply do:

factory.define('User', User, {
  ...
  profileImage: factory.assoc('ProfileImage', 'id')
});

factory-girl will now create a ProfileImage instance and place its id attribute in the profileImage attribute of the created User instance.

What if you want the ProfileImage instance itself to be assigned to profileImage? Just don't pass 'id' and the ProfileImage instance itself will be assigned to the profileImage attribute.

Note that factory.assoc will persist the model instance to DB. In case you don't want the model to be persisted, use factory.assocAttrs which just builds the attributes and does not persist a model to the DB.

At times you may want to associate more than one model instance. For example, let's say we want our users to have a list of addresses. Assuming we already have an Address model, we first define the Address factory:

factory.define('Address', Address, {
  id: factory.sequence('Address.id'),
  address1: factory.chance('address'),
  address2: factory.chance('street'),
  city: factory.chance('city', { country: 'us' }),
  state: factory.chance('state', { country: 'us' }),
  country: 'USA'
});

Now, we can tell factory-girl to associate multiple addresses with our User instances:

factory.define('User', User, {
  ...
  addresses: factory.assocMany('Address', 3)
});

Similar to factory.assocAttrs we have factory.assocAttrsMany to associate non-persisted models or their attributes.

So far so good! We can already see factory-girl making our life easier to build model instances. But, it is still limited to some extent. What if you have 20 test cases for users with expired password? It's going to be tedious to override the passwordExpiry attribute for each of those test cases. Add a few more attributes that may change with different test cases and things may soon get out of hand.

factory-girl allows you to define model factories with an initializer function instead of an object. To get started, we just make a simple change:

factory.define('User', User, (buildOptions) => {
  return {
    email: factory.sequence('user.email', n => `dummy-user-${n}@my-domain.com`),
    password: factory.chance('word', { syllables: 4 }),
    name: factory.chance('name', { middle: true }),
    about: factory.chance('paragraph', { sentences: 2 }),
    passwordExpiry: () => moment().add('1 month').toDate(),
    favoriteJoke: () => {
      fetch('http://api.icndb.com/jokes/random')
        .then(response => response.value.joke)
    },
    profileImage: factory.assoc('ProfileImage', 'id'),
    addresses: factory.assocMany('Address', 3, 'id')
  };
});

Notice that instead of an initializer object, we now have a function that takes an argument buildOptions and returns the initializer object. The buildOptions can be specified when requesting factory-girl to create an instance. Using the initializer function you can customise your models any way you want.

Let's first modify our factory a bit to use buildOptions, then we'll see how to pass buildOptions:

factory.define('User', User, (buildOptions = {}) => {
  const attrs = {
    ...
    addresses: factory.assocMany('Address', buildOptions.addressCount || 3, 'id')
  };
  if (buildOptions.passwordExpired) {
    attrs['passwordExpiry'] = moment().subtract('1 month').toDate(),
  }
  return attrs;
});

Now, when requesting factory-girl to create a User instance, we can do:

factory.create('User', {}, { passwordExpired: true, addressCount: 4 })
  .then(user => console.log(user));

Which would result in something like:

User {
  email: 'dummy-user-1@my-domain.com',
  password: 'tavnamgi',
  name: 'Nelgatwu Powuku Heup',
  about: 'Idefeulo foc omoemowa wahteze liv juvde puguprof epehuji upuga zige odfe igo sit pilamhul oto ukurecef.',
  passwordExpiry: 'May 22, 2016' // date formatting may be changed,
  favoriteJoke: 'Chuck Norris has two speeds: Walk and Kill.',
  profileImage: 1, // id of the ProfileImage instance created
  addresses: [1, 2, 3, 4] // ids of the Address instances created
}