Created
January 5, 2014 06:26
-
-
Save smackesey/8265099 to your computer and use it in GitHub Desktop.
Code for automated generation of a basic test suite for a yeoman generator. generator-ember is used as an example.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPEC OF GENERATOR EMBER | |
// this object includes information about generator dependencies, expected | |
// files, and options. This data is needed for the dynamic generation of tests. | |
// This spec is only provided here for convenience; normally it would reside in | |
// a separate file from the logic used for test generation. All code after the spec | |
// constitutes a module whose functions should be run in the context of the spec. | |
var data = { | |
scriptsDir: 'app/scripts', | |
templatesDir: 'app/templates', | |
generators: [ | |
'app', | |
'controller', | |
'view', | |
'model', | |
'router' | |
], | |
firstOrderDependencies: { | |
app: [], | |
controller: ['view', 'router'], | |
view: [], | |
model: ['controller'], | |
router: [] | |
}, | |
firstOrderExpectedFiles: { | |
app: { | |
misc: [ | |
'.gitignore', | |
'.gitattributes', | |
'.bowerrc', | |
'bower.json', | |
'package.json', | |
'.jshintrc', | |
'.editorconfig', | |
'Gruntfile.js', | |
'app/index.html' | |
], | |
scripts: [ | |
'app/scripts/app', | |
'app/scripts/store', | |
'app/scripts/routes/application_route', | |
], | |
templates: [ | |
'app/templates/application', | |
'app/templates/index', | |
] | |
}, | |
view: { | |
scripts: [ | |
'views/user_view', | |
'views/user_edit_view', | |
'views/users_view', | |
], | |
templates: [ | |
'user', | |
'user/edit', | |
'users' | |
] | |
}, | |
model: { | |
scripts: [ | |
'models/user_model.js' | |
] | |
}, | |
controller: { | |
scripts: [ | |
'controllers/users_controller', | |
'controllers/user_edit_controller', | |
'routes/user_route', | |
'routes/user_edit_route', | |
'routes/users_route' | |
] | |
}, | |
router: { | |
scripts: [ | |
'router' | |
] | |
} | |
}, | |
possibleOptions: { | |
app: { | |
coffee: [false, true], | |
emberscript: [false, true], | |
emblem: [false, true], | |
compassBootstrap: [false, true], | |
'skip-install': [true] | |
}, | |
view: { | |
coffee: [false, true], | |
emberscript: [false, true], | |
emblem: [false, true], | |
}, | |
model: { | |
coffee: [false, true], | |
emberscript: [false, true], | |
emblem: [false, true], | |
}, | |
controller: { | |
coffee: [false, true], | |
emberscript: [false, true], | |
emblem: [false, true], | |
}, | |
router: { | |
coffee: [false, true], | |
emberscript: [false, true] | |
}, | |
}, | |
optionDependencies: { | |
coffee: { | |
npmModules: ['grunt-contrib-coffee'], | |
gruntTasks: ['coffee', 'coffee-test'] | |
}, | |
compass: { | |
npmModules: ['grunt-contrib-compass'], | |
gruntTasks: ['compass'] | |
}, | |
emblem: { | |
npmModules: ['emblem', 'grunt-emblem'], | |
gruntTasks: ['emblem'] | |
}, | |
emberscript: { | |
npmModules: ['grunt-ember-script'], | |
gruntTasks: ['emberscript'] | |
} | |
}, | |
promptOnlyOptions: ['compassBootstrap'] | |
} | |
// CREATING EXPECTED FILES | |
// | |
// The current implementation of checking for expected files at generator-ember | |
// is not good. There are two issues: | |
// | |
// - file extensions change when one uses the coffeescript option as opposed to | |
// JS. This problem only gets larger when other JS and templating alternatives | |
// are supported. | |
// - some of the ember sub-generators have other generators as | |
// dependencies. Currently, these dependencies need to be specified in multiple | |
// places-- to bulid the expected file list for a generator with dependencies, | |
// you have to manually grab the expected files of those dependencies. | |
// | |
// The below solution can reads the generator spec to dynamically generate a | |
// list of expected files for any configuration. | |
exports.getDependencies = function (generator) { | |
var deps = [generator]; | |
if (this.firstOrderDependencies[generator].length !== 0) { | |
var firstOrderOtherDeps = this.firstOrderDependencies[generator]; | |
var allOtherDeps = _.chain(firstOrderOtherDeps) | |
.map(getDependencies.bind(this)) | |
.reduce(function (allDeps, newDeps) { | |
return allDeps.concat(newDeps); | |
}, []) | |
.uniq() | |
.value(); | |
deps = deps.concat(allOtherDeps); | |
} | |
return deps; | |
}; | |
function resolvePath(filepath, prefix, extension) { | |
return path.join(prefix, filepath + '.' + extension); | |
} | |
function getScriptExtension(options) { | |
if (options.coffee) { return 'coffee'; } else { | |
if (options.emberscript) { return 'em'; } else { | |
return 'js'; | |
} | |
} | |
} | |
function getTemplateExtension(options) { | |
if (options.emblem) { return 'emblem'; } else { | |
return 'js'; | |
} | |
} | |
exports.getExpectedFilesBare = function (generator) { | |
var allGenerators = this.getDependencies(generator); | |
return _.reduce(allGenerators, function (fileTable, gen) { | |
var genFileTable = this.firstOrderExpectedFiles[gen]; | |
for (var fileType in genFileTable) { | |
fileTable[fileType] = (fileTable[fileType] || []).concat(genFileTable[fileType]); | |
} | |
return fileTable; | |
}, {}, this); | |
}; | |
exports.getExpectedFiles = function (generator, options) { | |
var fileTable = this.getExpectedFilesBare(generator); | |
fileTable.scripts = _.map(fileTable.scripts, function (file) { | |
return resolvePath(file, this.scriptsDir, getScriptExtension(options)); | |
}, this); | |
fileTable.templates = _.map(fileTable.templates, function (file) { | |
return resolvePath(file, this.templatesDir, getTemplateExtension(options)); | |
}, this); | |
return _.reduce(_.values(fileTable), function (old, curr) { | |
return old.concat(curr); | |
}, []); | |
}; | |
// CHECKING OPTION CONFIGURATION AND DEPENDENCIES | |
// This is surely a need common to many generators. When the coffee option is | |
// set, grunt-contrib-coffee needs to be put in the package.json and a | |
// configuration for it set in the Gruntfile. The same holds for | |
// compassBootstrap etc. One problem with testing this is that there's not a | |
// very obvious place to put the tests-- you need to run the full generator to | |
// check, but most of the generator configuration is actually irrelevant to | |
// these tests. | |
// | |
// I implemented a solution to this problem along the lines of my solution for | |
// the expected files. The dependencies for each option are specified in the | |
// generator spec. Tests for them can then be automatically generated by the | |
// code below. | |
function checkNpmDependency(depName, negate) { | |
if (negate) { | |
return it.bind(this, 'does not add ' + depName + ' as a dependency', function () { | |
helpers.assertNoFileContent('package.json', new RegExp(depName)); | |
}); | |
} else { | |
return it.bind(this, 'adds ' + depName + ' as a dependency', function () { | |
helpers.assertFileContent('package.json', new RegExp(depName)); | |
}); | |
} | |
} | |
function checkGruntConfig(taskName, negate) { | |
var desc; | |
if (negate) { | |
desc = 'does not add grunt configuration for ' + taskName; | |
return it.bind(this, desc, function () { | |
helpers.assertNoFileContent('Gruntfile.js', new RegExp(taskName + ':')); | |
}); | |
} else { | |
desc = 'add grunt configuration for ' + taskName; | |
return it.bind(this, desc, function () { | |
helpers.assertFileContent('Gruntfile.js', new RegExp(taskName + ':')); | |
}); | |
} | |
} | |
exports.createOptionDependencyChecks = function () { | |
var optionTests = {}; | |
_.pairs(this.optionDeps).forEach(function (pair) { | |
var key = pair[1], val = pair[2]; | |
optionTests[key] = {true: [], false: []}; | |
val.npmModules.forEach(function (mod) { | |
optionTests[key][true].push(checkNpmDependency(mod)); | |
optionTests[key][false].push(checkNpmDependency(mod, true)); | |
}); | |
val.gruntTasks.forEach(function (task) { | |
optionTests[key][true].push(checkGruntConfig(task)); | |
optionTests[key][false].push(checkGruntConfig(task, true)); | |
}); | |
}); | |
return optionTests; | |
}; | |
exports.getOptionTests = function (generator) { | |
if (generator !== 'app') { | |
return this.createOptionDependencyChecks(); | |
} else { | |
return {}; | |
} | |
}; | |
/* GENERATING THE TEST SUITE | |
* --- | |
* The possible configurations of each generator are specified in | |
* the generator spec. This is an object having each key set to | |
* the name of a generator G. Each value is an object having keys that are the | |
* possible options for G, and values that are arrays of the possible values | |
* for that option. The set of all possible combinations of choices of option | |
* values is the set of possible configurations for G. The below example shows | |
* `possibleOptions` for just the `app` generator. | |
* | |
possibleOptions: { | |
app: { | |
coffee: [false, true], | |
emberscript: [false, true], | |
emblem: [false, true], | |
compassBootstrap: [false, true], | |
'skip-install': [true] | |
}, | |
... | |
} | |
* | |
* Because testing every possible combination of options is likely to be slow, | |
* the strategy taken here is to assume that the options are independent of one | |
* another. Each value for each option is tested at least once, but not in each | |
* of the possible contexts given by settings of the other options. | |
* | |
* To implement this, a 'base' configuration is formed by taking the first | |
* value from each option. For the `app` generator above, the base | |
* configuration would be: | |
* | |
* { | |
* coffee: false, emberscript: false, emblem: false, | |
* compassBootstrap: false, 'skip-install': true | |
* } | |
* | |
* A set of configurations to test is created by iterating over all possible | |
* values of one option at a time, with the rest of the options set to their | |
* values in the base. Thus, for the `app` generator above, 5 configurations | |
* are tested: the base configuration, and then the base configuration modified | |
* with a `true` value for each of the `coffee`, `emberscript`, `emblem`, and | |
* `compassBootstrap` options. | |
* | |
* A group of tests is then associated with each configuration. All | |
* configurations are tested for generation of expected files. Tests of options | |
* are assigned to the first configuration that fits the option value being | |
* tested. | |
*/ | |
function checkRequire(generator) { | |
return it.bind(this, 'can be required', function () { | |
require('../' + generator); | |
}); | |
} | |
exports.getOptionSets = function (generator) { | |
var baseConfig = _.mapValues(this.possibleOptions[generator], function (val, key) { | |
return _.first(val); | |
}); | |
var alts = _.mapValues(this.possibleOptions[generator], function (val, key) { | |
return _.drop(val, 1); | |
}); | |
var configs = [baseConfig]; | |
_.pairs(alts).forEach(function (pair) { | |
var key = pair[0], possibleValues = pair[1]; | |
var altConfigs = _.map(possibleValues, function (val) { | |
var config = _.clone(baseConfig); | |
config[key] = val; | |
return config; | |
}); | |
configs = configs.concat(altConfigs); | |
}); | |
return configs; | |
}; | |
exports.createGenerator = function (generator, options, args) { | |
var deps = _.map(this.getDependencies(generator), function (dep) { return '../../' + dep; }); | |
var fullName = 'ember:' + generator; | |
var gen = helpers.createGenerator(fullName, deps, args, options); | |
for (var opt in options) { | |
if (this.promptOnlyOptions.indexOf(opt) === -1) { | |
gen.options[opt] = options[opt]; | |
} else { | |
var promptOpts = {}; | |
promptOpts[opt] = options[opt]; | |
helpers.mockPrompt(gen, promptOpts); | |
} | |
} | |
return gen; | |
}; | |
exports.createTestGroups = function (generator) { | |
var optionSets = this.getOptionSets(generator); | |
var optionTests = this.getOptionTests(generator); | |
var testGroups = _.map(optionSets, function (optSet) { | |
return { generator: generator, options: optSet, tests: [] }; | |
}); | |
for (var group in testGroups) { | |
group.tests.push(this.checkExpectedFiles(generator, group.options)); | |
} | |
for (var option in _.keys(optionTests)) { | |
for (var value in optionTests[option]) { | |
group = testGroups.find(function (grp) { | |
return grp.options[option] === value; | |
}); | |
group.tests = group.tests.concat(optionTests[option][value]); | |
} | |
} | |
return testGroups; | |
}; | |
exports.testGenerator = function (generator) { | |
var testGroups = this.createTestGroups(generator); | |
describe(generator + " generator", function () { | |
checkRequire(generator)(); | |
testGroups.forEach(function (group) { | |
before(function () { | |
var gen = this.createGenerator(generator, group.options); | |
gen.run(); | |
}); | |
group.tests.forEach(function (test) { test(); }); | |
}); | |
}); | |
}; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment