Skip to content
This repository was archived by the owner on May 29, 2019. It is now read-only.

Commit 88cdb97

Browse files
committed
Merge pull request #2 from strongloop/feature/test-data-builder
Implement TestDataBuilder
2 parents 8d57e1f + e8abd6e commit 88cdb97

File tree

4 files changed

+286
-2
lines changed

4 files changed

+286
-2
lines changed

index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
var _describe = {};
22
var _it = {};
33
var _beforeEach = {};
4-
var helpers = module.exports = {
4+
var helpers = exports = module.exports = {
55
describe: _describe,
66
it: _it,
77
beforeEach: _beforeEach
@@ -260,3 +260,5 @@ function(credentials, verb, url) {
260260
_it.shouldBeDenied();
261261
});
262262
}
263+
264+
exports.TestDataBuilder = require('./lib/test-data-builder');

lib/test-data-builder.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
var extend = require('util')._extend;
2+
var async = require('async');
3+
4+
module.exports = exports = TestDataBuilder;
5+
6+
/**
7+
* Build many Model instances in one async call.
8+
*
9+
* Usage:
10+
* ```js
11+
* // The context object to hold the created models.
12+
* // You can use `this` in mocha test instead.
13+
* var context = {};
14+
*
15+
* var ref = TestDataBuilder.ref;
16+
* new TestDataBuilder()
17+
* .define('application', Application, {
18+
* pushSettings: { stub: { } }
19+
* })
20+
* .define('device', Device, {
21+
* appId: ref('application.id'),
22+
* deviceType: 'android'
23+
* })
24+
* .define('notification', Notification)
25+
* .buildTo(context, function(err) {
26+
* // test models are available as
27+
* // context.application
28+
* // context.device
29+
* // context.notification
30+
* });
31+
* ```
32+
* @constructor
33+
*/
34+
function TestDataBuilder() {
35+
this._definitions = [];
36+
}
37+
38+
/**
39+
* Define a new model instance.
40+
* @param {string} name Name of the instance.
41+
* `buildTo()` will save the instance created as context[name].
42+
* @param {constructor} Model Model class/constructor.
43+
* @param {Object.<string, Object>=} properties
44+
* Properties to set in the object.
45+
* Intelligent default values are supplied by the builder
46+
* for required properties not listed.
47+
* @return TestDataBuilder (fluent interface)
48+
*/
49+
TestDataBuilder.prototype.define = function(name, Model, properties) {
50+
this._definitions.push({
51+
name: name,
52+
model: Model,
53+
properties: properties
54+
});
55+
return this;
56+
};
57+
58+
/**
59+
* Reference the value of a property from a model instance defined before.
60+
* @param {string} path Generally in the form '{name}.{property}', where {name}
61+
* is the name passed to `define()` and {property} is the name of
62+
* the property to use.
63+
*/
64+
TestDataBuilder.ref = function(path) {
65+
return new Reference(path);
66+
};
67+
68+
/**
69+
* Asynchronously build all models defined via `define()` and save them in
70+
* the supplied context object.
71+
* @param {Object.<string, Object>} context The context to object to populate.
72+
* @param {function(Error)} callback Callback.
73+
*/
74+
TestDataBuilder.prototype.buildTo = function(context, callback) {
75+
this._context = context;
76+
async.eachSeries(
77+
this._definitions,
78+
this._buildObject.bind(this),
79+
callback);
80+
};
81+
82+
TestDataBuilder.prototype._buildObject = function(definition, callback) {
83+
var defaultValues = this._gatherDefaultPropertyValues(definition.model);
84+
var values = extend(defaultValues, definition.properties || {});
85+
var resolvedValues = this._resolveValues(values);
86+
87+
definition.model.create(resolvedValues, function(err, result) {
88+
if (err) {
89+
console.error(
90+
'Cannot build object %j - %s\nDetails: %j',
91+
definition,
92+
err.message,
93+
err.details);
94+
} else {
95+
this._context[definition.name] = result;
96+
}
97+
98+
callback(err);
99+
}.bind(this));
100+
};
101+
102+
TestDataBuilder.prototype._resolveValues = function(values) {
103+
var result = {};
104+
for (var key in values) {
105+
var val = values[key];
106+
if (val instanceof Reference) {
107+
val = values[key].resolveFromContext(this._context);
108+
}
109+
result[key] = val;
110+
}
111+
return result;
112+
};
113+
114+
var valueCounter = 0;
115+
TestDataBuilder.prototype._gatherDefaultPropertyValues = function(Model) {
116+
var result = {};
117+
Model.forEachProperty(function createDefaultPropertyValue(name) {
118+
var prop = Model.definition.properties[name];
119+
if (!prop.required) return;
120+
121+
switch (prop.type) {
122+
case String:
123+
result[name] = 'a test ' + name + ' #' + (++valueCounter);
124+
break;
125+
case Number:
126+
result[name] = 1230000 + (++valueCounter);
127+
break;
128+
case Date:
129+
result[name] = new Date(
130+
2222, 12, 12, // yyyy, mm, dd
131+
12, 12, 12, // hh, MM, ss
132+
++valueCounter // milliseconds
133+
);
134+
break;
135+
case Boolean:
136+
// There isn't much choice here, is it?
137+
// Let's use "false" to encourage users to be explicit when they
138+
// require "true" to turn some flag/behaviour on
139+
result[name] = false;
140+
break;
141+
// TODO: support nested structures - array, object
142+
}
143+
});
144+
return result;
145+
};
146+
147+
/**
148+
* Placeholder for values that will be resolved during build.
149+
* @param path
150+
* @constructor
151+
* @private
152+
*/
153+
function Reference(path) {
154+
this._path = path;
155+
}
156+
157+
Reference.prototype.resolveFromContext = function(context) {
158+
var elements = this._path.split('.');
159+
160+
var result = elements.reduce(
161+
function(obj, prop) {
162+
return obj[prop];
163+
},
164+
context
165+
);
166+
167+
return result;
168+
};

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
"supertest": "~0.8.2",
1414
"mocha": "~1.15.1",
1515
"loopback-datasource-juggler": "~1.2.7",
16-
"loopback": "~1.3.3"
16+
"loopback": "~1.3.3",
17+
"async": "~0.2.9"
18+
},
19+
"devDependencies": {
20+
"chai": "~1.8.1"
1721
}
1822
}

test/test-data-builder.test.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
var loopback = require('loopback');
2+
var TestDataBuilder = require('../lib/test-data-builder');
3+
var expect = require('chai').expect;
4+
5+
describe('TestDataBuilder', function() {
6+
var db;
7+
var TestModel;
8+
9+
beforeEach(function() {
10+
db = loopback.createDataSource({ connector: loopback.Memory });
11+
});
12+
13+
it('builds a model', function(done) {
14+
givenTestModel({ value: String });
15+
16+
new TestDataBuilder()
17+
.define('model', TestModel, { value: 'a-string-value' })
18+
.buildTo(this, function(err) {
19+
if (err) return done(err);
20+
expect(this.model).to.have.property('value', 'a-string-value');
21+
done();
22+
}.bind(this));
23+
});
24+
25+
// Parameterized test
26+
function itAutoFillsRequiredPropertiesWithUniqueValuesFor(type) {
27+
it(
28+
'auto-fills required ' + type + ' properties with unique values',
29+
function(done) {
30+
givenTestModel({
31+
required1: { type: type, required: true },
32+
required2: { type: type, required: true }
33+
});
34+
35+
new TestDataBuilder()
36+
.define('model', TestModel, {})
37+
.buildTo(this, function(err) {
38+
if (err) return done(err);
39+
expect(this.model.required1).to.not.equal(this.model.required2);
40+
expect(this.model.optional).to.satisfy(notSet);
41+
done();
42+
}.bind(this));
43+
}
44+
);
45+
}
46+
47+
itAutoFillsRequiredPropertiesWithUniqueValuesFor(String);
48+
itAutoFillsRequiredPropertiesWithUniqueValuesFor(Number);
49+
itAutoFillsRequiredPropertiesWithUniqueValuesFor(Date);
50+
51+
it('auto-fills required Boolean properties with false', function(done) {
52+
givenTestModel({
53+
required: { type: Boolean, required: true }
54+
});
55+
56+
new TestDataBuilder()
57+
.define('model', TestModel, {})
58+
.buildTo(this, function(err) {
59+
if (err) return done(err);
60+
expect(this.model.required).to.equal(false);
61+
done();
62+
}.bind(this));
63+
});
64+
65+
it('does not fill optional properties', function(done) {
66+
givenTestModel({
67+
optional: { type: String, required: false }
68+
});
69+
70+
new TestDataBuilder()
71+
.define('model', TestModel, {})
72+
.buildTo(this, function(err) {
73+
if (err) return done(err);
74+
expect(this.model.optional).to.satisfy(notSet);
75+
done();
76+
}.bind(this));
77+
});
78+
79+
it('resolves references', function(done) {
80+
var Parent = givenModel('Parent', { name: { type: String, required: true } });
81+
var Child = givenModel('Child', { parentName: String });
82+
83+
new TestDataBuilder()
84+
.define('parent', Parent)
85+
.define('child', Child, {
86+
parentName: TestDataBuilder.ref('parent.name')
87+
})
88+
.buildTo(this, function(err) {
89+
if(err) return done(err);
90+
expect(this.child.parentName).to.equal(this.parent.name);
91+
done();
92+
}.bind(this));
93+
});
94+
95+
function givenTestModel(properties) {
96+
TestModel = givenModel('TestModel', properties);
97+
}
98+
99+
function givenModel(name, properties) {
100+
var ModelCtor = loopback.createModel(name, properties);
101+
ModelCtor.attachTo(db);
102+
return ModelCtor;
103+
}
104+
105+
function notSet(value) {
106+
// workaround for `expect().to.exist` that triggers a JSHint error
107+
// (a no-op statement discarding the property value)
108+
return value === undefined || value === null;
109+
}
110+
});

0 commit comments

Comments
 (0)