James Croft

Factories for testing JavaScript

When writing tests for Rails apps I tend to use Factory Girl to build the objects that I use in the test suite. Using it means that I don’t have to worry about having to construct objects which pass the model validations, I can just call the factory.

To use Factory Girl, you first create a factory definition. At the most basic level this can be a list of default attributes for a class.

FactoryGirl.define do
  factory :post, :class => Post do
    # the 'title' attribute is required for all posts
    title 'A title'

  factory :approved_post, :parent => :post do
    approved true

Then, in your test suite, you can call these factories to produce valid instances of the class. You don’t have to worry about manually providing the all attributes required to pass the model validations.

Factory(:post) # => Returns a Post instance with the title already set
Factory(:approved_post) # => Returns a Post instance with the title and approved flag already set

I’ve been wanting something similar for my JavaScript test suites. In particular I want:

  1. Factories to create specific objects with default values.
  2. An easy way to define the factories.
  3. The ability for a factories to inherit their defaults values from another factory. (Similar to :parent in Factory Girl)

This is what I came up with.

factory = (originalClass, opts) ->
  defaults = _.extend({}, opts.parent?.defaults, opts.defaults)
  F = (args = {}) -> 
    new originalClass(_.extend({}, defaults, args))
  F.defaults = defaults

A couple of notes:

  1. It uses the Underscore JavaScript library for the extend call. This is fine for me as underscore is included in the project that I’m working on.
  2. It assumes that the constructor of the object that you ultimately want returned takes an object eg. a JSON hash of attributes. Again, this is fine for my purposes. It would be trivial to implement some type checking if you were dealing with a class that expected something different.

I like how simple the code is. It’s only basic stuff but it meets the requirements above. What follows is a demonstration of how you would use this.

Consider the following models which have some very basic validations.

M = {} # Namespace for models
M.Vehicle = class Vehicle 
  constructor: (@attributes={}) ->
    if 'registration' not of @attributes
      throw 'All vehicles must have a registration number'
  get: (attr) -> @attributes[attr]
  set: (attr,val) -> @attributes[attr] = val

M.Truck = class Truck extends Vehicle
  constructor: (@attributes={}) ->
    if 'weightLimit' not of @attributes
      throw 'Trucks must have a weight limit'

You could define your factories as follows

F = {} # Namespace for factories
F.Vehicle = factory M.Vehicle,
    registration: 'P717 MLL'

F.MovingVehicle = factory M.Vehicle,
  parent: F.Vehicle
    speed: 70

F.Truck = factory M.Truck,
    registration: 'R123 YTH'
    weightLimit: '17 tonne'

F.BrokenDownTruck = factory M.Truck,
  parent: F.Truck
    speed: 0
    engine: 'On fire'

Factories are defined by a call to the factory function which has two arguments.

  1. The constructor function that will produce the object you want returned (analagous to :class => in Factory Girl).
  2. An options object containing the defaults for the class (the defaults property) and an optional parent property. The parent property is the name of the factory you want to inherit from (analagous to :parent => in Factory Girl).

You can then use the factories in your test suite to easily build valid objects.

vehicle = new F.Vehicle
vehicle instanceof M.Vehicle # => true
vehicle.get('registration') # => 'P717 MLL'

movingVehicle = new F.MovingVehicle
movingVehicle instanceof M.Vehicle # => true
movingVehicle.get('speed') # => 70

# The defaults can be overidden when the factory is called
truck = new F.Truck(registration: 'T281 ATB')
truck.get('registration') # => 'T281 ATB'

These are simple examples but they demonstrates the point. Whilst simple, this factory code has really helped keep things slim in situations where I am dealing with lots of models with lots of validations.