Skip to content
This repository was archived by the owner on May 29, 2019. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
var _describe = {};
var _it = {};
var _beforeEach = {};
var helpers = module.exports = {
var helpers = exports = module.exports = {
describe: _describe,
it: _it,
beforeEach: _beforeEach
Expand Down Expand Up @@ -260,3 +260,5 @@ function(credentials, verb, url) {
_it.shouldBeDenied();
});
}

exports.TestDataBuilder = require('./lib/test-data-builder');
168 changes: 168 additions & 0 deletions lib/test-data-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
var extend = require('util')._extend;
var async = require('async');

module.exports = exports = TestDataBuilder;

/**
* Build many Model instances in one async call.
*
* Usage:
* ```js
* // The context object to hold the created models.
* // You can use `this` in mocha test instead.
* var context = {};
*
* var ref = TestDataBuilder.ref;
* new TestDataBuilder()
* .define('application', Application, {
* pushSettings: { stub: { } }
* })
* .define('device', Device, {
* appId: ref('application.id'),
* deviceType: 'android'
* })
* .define('notification', Notification)
* .buildTo(context, function(err) {
* // test models are available as
* // context.application
* // context.device
* // context.notification
* });
* ```
* @constructor
*/
function TestDataBuilder() {
this._definitions = [];
}

/**
* Define a new model instance.
* @param {string} name Name of the instance.
* `buildTo()` will save the instance created as context[name].
* @param {constructor} Model Model class/constructor.
* @param {Object.<string, Object>=} properties
* Properties to set in the object.
* Intelligent default values are supplied by the builder
* for required properties not listed.
* @return TestDataBuilder (fluent interface)
*/
TestDataBuilder.prototype.define = function(name, Model, properties) {
this._definitions.push({
name: name,
model: Model,
properties: properties
});
return this;
};

/**
* Reference the value of a property from a model instance defined before.
* @param {string} path Generally in the form '{name}.{property}', where {name}
* is the name passed to `define()` and {property} is the name of
* the property to use.
*/
TestDataBuilder.ref = function(path) {
return new Reference(path);
};

/**
* Asynchronously build all models defined via `define()` and save them in
* the supplied context object.
* @param {Object.<string, Object>} context The context to object to populate.
* @param {function(Error)} callback Callback.
*/
TestDataBuilder.prototype.buildTo = function(context, callback) {
this._context = context;
async.eachSeries(
this._definitions,
this._buildObject.bind(this),
callback);
};

TestDataBuilder.prototype._buildObject = function(definition, callback) {
var defaultValues = this._gatherDefaultPropertyValues(definition.model);
var values = extend(defaultValues, definition.properties || {});
var resolvedValues = this._resolveValues(values);

definition.model.create(resolvedValues, function(err, result) {
if (err) {
console.error(
'Cannot build object %j - %s\nDetails: %j',
definition,
err.message,
err.details);
} else {
this._context[definition.name] = result;
}

callback(err);
}.bind(this));
};

TestDataBuilder.prototype._resolveValues = function(values) {
var result = {};
for (var key in values) {
var val = values[key];
if (val instanceof Reference) {
val = values[key].resolveFromContext(this._context);
}
result[key] = val;
}
return result;
};

var valueCounter = 0;
TestDataBuilder.prototype._gatherDefaultPropertyValues = function(Model) {
var result = {};
Model.forEachProperty(function createDefaultPropertyValue(name) {
var prop = Model.definition.properties[name];
if (!prop.required) return;

switch (prop.type) {
case String:
result[name] = 'a test ' + name + ' #' + (++valueCounter);
break;
case Number:
result[name] = 1230000 + (++valueCounter);
break;
case Date:
result[name] = new Date(
2222, 12, 12, // yyyy, mm, dd
12, 12, 12, // hh, MM, ss
++valueCounter // milliseconds
);
break;
case Boolean:
// There isn't much choice here, is it?
// Let's use "false" to encourage users to be explicit when they
// require "true" to turn some flag/behaviour on
result[name] = false;
break;
// TODO: support nested structures - array, object
}
});
return result;
};

/**
* Placeholder for values that will be resolved during build.
* @param path
* @constructor
* @private
*/
function Reference(path) {
this._path = path;
}

Reference.prototype.resolveFromContext = function(context) {
var elements = this._path.split('.');

var result = elements.reduce(
function(obj, prop) {
return obj[prop];
},
context
);

return result;
};
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
"supertest": "~0.8.2",
"mocha": "~1.15.1",
"loopback-datasource-juggler": "~1.2.7",
"loopback": "~1.3.3"
"loopback": "~1.3.3",
"async": "~0.2.9"
},
"devDependencies": {
"chai": "~1.8.1"
}
}
110 changes: 110 additions & 0 deletions test/test-data-builder.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
var loopback = require('loopback');
var TestDataBuilder = require('../lib/test-data-builder');
var expect = require('chai').expect;

describe('TestDataBuilder', function() {
var db;
var TestModel;

beforeEach(function() {
db = loopback.createDataSource({ connector: loopback.Memory });
});

it('builds a model', function(done) {
givenTestModel({ value: String });

new TestDataBuilder()
.define('model', TestModel, { value: 'a-string-value' })
.buildTo(this, function(err) {
if (err) return done(err);
expect(this.model).to.have.property('value', 'a-string-value');
done();
}.bind(this));
});

// Parameterized test
function itAutoFillsRequiredPropertiesWithUniqueValuesFor(type) {
it(
'auto-fills required ' + type + ' properties with unique values',
function(done) {
givenTestModel({
required1: { type: type, required: true },
required2: { type: type, required: true }
});

new TestDataBuilder()
.define('model', TestModel, {})
.buildTo(this, function(err) {
if (err) return done(err);
expect(this.model.required1).to.not.equal(this.model.required2);
expect(this.model.optional).to.satisfy(notSet);
done();
}.bind(this));
}
);
}

itAutoFillsRequiredPropertiesWithUniqueValuesFor(String);
itAutoFillsRequiredPropertiesWithUniqueValuesFor(Number);
itAutoFillsRequiredPropertiesWithUniqueValuesFor(Date);

it('auto-fills required Boolean properties with false', function(done) {
givenTestModel({
required: { type: Boolean, required: true }
});

new TestDataBuilder()
.define('model', TestModel, {})
.buildTo(this, function(err) {
if (err) return done(err);
expect(this.model.required).to.equal(false);
done();
}.bind(this));
});

it('does not fill optional properties', function(done) {
givenTestModel({
optional: { type: String, required: false }
});

new TestDataBuilder()
.define('model', TestModel, {})
.buildTo(this, function(err) {
if (err) return done(err);
expect(this.model.optional).to.satisfy(notSet);
done();
}.bind(this));
});

it('resolves references', function(done) {
var Parent = givenModel('Parent', { name: { type: String, required: true } });
var Child = givenModel('Child', { parentName: String });

new TestDataBuilder()
.define('parent', Parent)
.define('child', Child, {
parentName: TestDataBuilder.ref('parent.name')
})
.buildTo(this, function(err) {
if(err) return done(err);
expect(this.child.parentName).to.equal(this.parent.name);
done();
}.bind(this));
});

function givenTestModel(properties) {
TestModel = givenModel('TestModel', properties);
}

function givenModel(name, properties) {
var ModelCtor = loopback.createModel(name, properties);
ModelCtor.attachTo(db);
return ModelCtor;
}

function notSet(value) {
// workaround for `expect().to.exist` that triggers a JSHint error
// (a no-op statement discarding the property value)
return value === undefined || value === null;
}
});