Test Context Builder

Test context builder is an npm package I wrote to complement db-fabricator. The main purpose of db-fabricator is to setup test context data for integration testing. However, setting up test context data gets repetitive and verbose for each test. In many cases you have the same basic object that most of your tests need. So it would be nice to be able to define a set of basic contexts that you often use and be able to build on top of it. This is what test-context-builder is for.

Let’s jump to example to see what it can do.

import { Fabricator } from 'db-fabricator';
import { TestContext } from 'test-context-builder';
let [getId, fabricate] = [Fabricator.getId, Fabricator.fabricate];

TestContext.define('district', () => {
  let local: any = {};
  local.district = fabricate('district');
  local.school   = fabricate('school', { districtId: getId(local.district) });
  return local;
});

That example defines a context, named 'district', which will create a district and a school, when the context is loaded. You can name the context anything you want, but it has to be globally unique in your project. Refer to db-fabricator documentation for more detail on how to use DB Fabricator features. After we define the context, it can be loaded this way:

let tc = new TestContext();
tc.load('district').then(() => {
  tc.context.district; // the fabricated district object
  tc.context.school;
});

Now, let’s say you want to create a few users in that school. We can define 'users' context that creates a teacher and a student.

TestContext.define('users', ['district'], (context) => {
  let local: any = {};
  let schoolId = { schoolId: context.school.id };
  local.student = fabricate('student', schoolId);
  local.teacher = fabricate('teacher', schoolId);
  return local;
});

Notice that this context definition has ['district'] as the second argument. This means that 'users' context depends on 'district' context. When a context defines dependencies, it can assume that all objects created in the dependencies will be loaded and accessible through the first argument passed in the context definition function. In this example we can access the school by calling context.school. Now we can load the 'users' context and have all the objects from 'users' and 'district' contexts loaded into tc.context.

let tc = new TestContext();
tc.load('users').then(() => {
  tc.context; // will have all objects loaded by 'district' and 'users' context
});

Loading Multiple Contexts and Dependency Management

You can load multiple contexts at once by passing an array of context names to load function.

tc.load(['district1', 'district2']).then(() => {
  tc.context; // will have all objects loaded by 'district2' and 'district2' context
});

Test context builder will make sure that the same dependency will be loaded only once even when it’s specified as dependency by multiple contexts.

Let’s say we define 'users2' context that also depends on 'district' context.

TestContext.define('users2', ['district'], (context) => {
  let local: any = {};
  let schoolId = { schoolId: context.school.id };
  local.student2 = fabricate('student', schoolId);
  local.teacher2 = fabricate('teacher', schoolId);
  return local;
});

We can load both 'users' and 'users2' context, and have 'district' context be loaded only once. So all users will be created with the same school and district.

tc.load(['users', 'users2']).then(() => {
  tc.context; // will have all objects loaded by 'users', 'users2' and 'district' context
});

You can even pass 'district' to the load function and the result will be the same.

// these all do the same thing
tc.load(['users', 'users2']);
tc.load(['district', 'users', 'users2']);
tc.load(['users2', 'users', 'district']); // order does not matter

How is this really used?

So this looks cool, but how do I really use it in my test?

Here is some example of what my mocha test looks like with Test Context Builder and DB Fabricator.

// shared-context.ts

import { Fabricator } from 'db-fabricator';
import { TestContext } from 'test-context-builder';
import './fabricators.ts'; // assume this defines the fabricator templates
let [getId, fabricate] = [Fabricator.getId, Fabricator.fabricate];

TestContext.define('district', () => {
  let local: any = {};
  local.district = fabricate('district');
  local.school   = fabricate('school', { districtId: getId(local.district) });
  return local;
});

TestContext.define('users', ['district'], (context) => {
  let local: any = {};
  let schoolId = { schoolId: context.school.id };
  local.student = fabricate('student', schoolId);
  local.teacher = fabricate('teacher', schoolId);
  return local;
});

TestContext.define('users2', ['district'], (context) => {
  let local: any = {};
  let schoolId = { schoolId: context.school.id };
  local.student2 = fabricate('student', schoolId);
  local.teacher2 = fabricate('teacher', schoolId);
  return local;
});

// sometest.spec.ts

import { TestContext } from 'test-context-builder';
import './shared-context.ts';

describe('Some test', function() {
  let tc = new TestContext();
  let context;
  before(function() {
    // sometime when you have a lot of data to load, it takes more than the timeout limit
    // you can set timeout just for the before function
    // this will not change timeout for each individual test
    this.timeout(3000);
    // Clean database before loading test context
    return dbCleaner.clean().then(() => {
      return tc.load(['users']).then(() => {
        context = tc.context; // just for shortcut
      });
    });
  });
  
  it('works', function(done) {
    expect(context.student.schoolId).to.equal(context.school.id);
  });
});

describe('Another test', function() {
  let tc = new TestContext();
  let context;
  before(function() {
    return dbCleaner.clean().then(() => {
      return tc.load(['users', 'users2']).then(() => {
        context = tc.context; // just for shortcut
      });
    });
  });
  
  it('works', function(done) {
    expect(context.student.schoolId).to.equal(context.student2.schoolId);
  });
});

Installing

test-context-builder is available as npm package. You can install it by

$ npm install test-context-builder