Introducing DB Fabricator

When writing end to end tests, we often need to set up a context by creating some data in database. Each test case should start with a clean slate and have data that are created only for that test. So it’s important to make creating these test data as simple as possible.

For Rails project, I have been using Fabrication gem for generating testing data. I haven’t been able to find anything like it in node.js, and that’s why I created DB Fabricator.

At the core of it, DB Fabricator lets you define templates for different type of objects. You can then create objects in the database using the template. When creating the object, you can override the template attributes.

Here is an example of defining a template for user object. The template name is the table name in database.

Fabricator.template({
  name: 'user',
  attr: {
    firstName: 'Bob',
    lastName: 'Smith',
  }
});

Now, we can create new rows in the user table:

Fabricator.fabricate('user'); // Bob Smith
Fabricator.fabricate('user', { firstName: 'Jane' }); // Jane Smith

Composable Attributes

Let’s say you have a username attribute that has to be unique. It would be nice to be able to use the first and last name as part of the username, so we don’t have to pass the username every time we create a user. You can create template attribute that is composed from the other attributes, by setting the attribute as a function that will take an argument which is the object being created. The value returned from that function will be set to the attribute.

Fabricator.template({
  name: 'user',
  attr: {
    firstName: 'Bob',
    lastName: 'Smith',
    username: (obj) => `${obj.firstName}.${obj.lastName}`
  }
});

Fabricator.fabricate('user'); // username: Bob.Smith
Fabricator.fabricate('user', { firstName: 'Jane' }); // username: Jane.Smith

Nested Dependencies

Often, the object we need to create depends on oher objects to exist, which might in turn depend on other objects. For example, a user needs to have a department, and department needs to have an organization. It would be cumbersome to have to create 2 other objects when all you need is just a user. To make this simpler, DB Fabricator lets you define the template to automatically create the dependencies unless it’s overriden. By setting the attribute as a function that fabricate another object.

Fabricator.template({
  name: 'organization',
  attr: {
    name: 'Fabricator Inc'
  }
});
Fabricator.template({
  name: 'department',
  attr: {
    name: 'IT',
    organizationId: () => Fabricator.fabricate('organization')
  }
});
Fabricator.template({
  name: 'user',
  attr: {
    firstName: 'Bob',
    lastName: 'Smith',
    username: (obj) => `${obj.firstName}.${obj.lastName}`,
    departmentId: () => Fabricator.fabricate('department')
  }
});

Fabricator.fabricate('user'); // this will create a department, an organization and a user 'Bob Smith'

You can still create the dependencies instead of using the default from template. For example, if you need to create a few users that have the same department. Note that, because of the async nature of node.js, Fabricator.fabricate returns a promise.

Fabricator.fabricate('organization')
.then((org) => {
  Fabricator.fabricate('department', { name: 'IT', organizationId: org.id })
  .then((dept) => {
    Fabricator.fabricate('user', { firstName: 'Bob', departmentId: dept.id });
    Fabricator.fabricate('user', { firstName: 'Jane', departmentId: dept.id });
  })
  Fabricator.fabricate('department', { name: 'Marketing', organizationId: org.id })
  .then((dept) => {
    Fabricator.fabricate('user', { firstName: 'Jon', departmentId: dept.id });
    Fabricator.fabricate('user', { firstName: 'Mary', departmentId: dept.id });
  })
});

Update: Version 2.0 provides a more straightforward way to do the above. See DB Fabricator version 2.0

Nested Templates

You can create a template that uses another template as a starting point. For example, we have a few user types: ‘student’ and ‘teacher’. A teacher needs to have email with format ‘[email protected]’ while student email should look like ‘[email protected]’. We can set the type and email everytime we create a user.

Fabricator.fabricate('user', {
  firstName: 'Bob', type: 'teacher',
  email: (user) => { `${user.username}@teacher.school.edu` }
});
Fabricator.fabricate('user', {
  firstName: 'Joe', type: 'student',
  email: (user) => { `${user.username}@student.school.edu` }
});

But a more convenient way is to define a template for each user type. The template can be derived from the basic user template to minimize duplication. Just set the from attribute to the template name when defining a template. In this example the from is set to 'user'.

Fabricator.template({
  name: 'user-teacher',
  from: 'user',
  attr: {
    type: 'teacher',
    email: (user) => { `${user.username}@teacher.school.edu` }
  }
});
Fabricator.template({
  name: 'user-student',
  from: 'user',
  attr: {
    type: 'student',
    email: (user) => { `${user.username}@student.school.edu` }
  }
});

Fabricator.fabricate('user-teacher',{firstName:'Bob'}); //email:[email protected]
Fabricator.fabricate('user-student',{firstName:'Joe'}); //email:[email protected]

Template can be deeply nested. For example, we can define a template from ‘user-student’.

Fabricator.template({
  name: 'user-student-1st-grade',
  from: 'user-student',
  attr: {
    gradeLevel: '1'
  }
});

Extensible

Currently DB Fabricator only supports MySQL data store, but you can create an adaptor for any database. Just implement a class that implements the DataStoreAdaptor interface. For an example, see the MySQLAdaptor implementation.

For documentation, submitting issues, and contribution, visit DB Fabricator on Github.