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.
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, usefactory.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
}