├── .editorconfig ├── .gitignore ├── .jshintignore ├── .jshintrc ├── changelog.markdown ├── connection.js ├── drop.js ├── index.js ├── license ├── model-loader.js ├── mongotape.js ├── package.json ├── readme.markdown ├── state.js └── test └── drop.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | Thumbs.db 5 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | dist 4 | example 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "newcap": true, 5 | "noarg": true, 6 | "noempty": true, 7 | "nonew": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "trailing": true, 12 | "boss": true, 13 | "eqnull": true, 14 | "strict": true, 15 | "immed": true, 16 | "expr": true, 17 | "latedef": "nofunc", 18 | "quotmark": "single", 19 | "validthis": true, 20 | "indent": 2, 21 | "node": true, 22 | "browser": true 23 | } 24 | -------------------------------------------------------------------------------- /changelog.markdown: -------------------------------------------------------------------------------- 1 | # 1.0.0 IPO 2 | 3 | - Initial Public Release 4 | -------------------------------------------------------------------------------- /connection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var state = require('./state'); 4 | var socket = { 5 | socketOptions: { connectTimeoutMS: 2000, keepAlive: 1 } 6 | }; 7 | var options = { 8 | server: socket, 9 | replset: socket, 10 | db: { native_parser: true } 11 | }; 12 | 13 | function connect (done) { 14 | var uri = state.env('MONGO_URI'); 15 | var db = state.mongoose.connection; 16 | db.once('connected', done || noop); 17 | db.open(uri, options); 18 | } 19 | 20 | function disconnect (done) { 21 | var next = done || noop; 22 | var db = state.mongoose.connection; 23 | if (db.readyState !== 0) { 24 | db.once('disconnected', next); 25 | db.close(); 26 | } else { 27 | next(); 28 | } 29 | } 30 | 31 | function noop () {} 32 | 33 | module.exports = { 34 | connect: connect, 35 | disconnect: disconnect 36 | }; 37 | -------------------------------------------------------------------------------- /drop.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var state = require('./state'); 4 | var word = /\btest\b/i; 5 | 6 | // being able to drop the database is kind of a big deal 7 | // disallow outside of 'test' environment, and also outside of 'test' database 8 | function validate () { 9 | var env = state.env('NODE_ENV'); 10 | var uri = state.env('MONGO_URI'); 11 | var notTestEnv = env !== 'test'; 12 | if (notTestEnv) { 13 | throw new Error('NODE_ENV must be set to "test".'); 14 | } 15 | var notTestDb = word.test(uri) === false; 16 | if (notTestDb) { 17 | throw new Error('MONGO_URI must contain "test" word.'); 18 | } 19 | } 20 | 21 | function drop (done) { 22 | validate(); 23 | state.mongoose.connection.db.dropDatabase(done); 24 | } 25 | 26 | module.exports = drop; 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var state = require('./state'); 4 | var mongotape = require('./mongotape'); 5 | 6 | function setup (options) { 7 | state.configure(options); 8 | return mongotape; 9 | } 10 | 11 | module.exports = setup; 12 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2015 Nicolas Bevacqua 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /model-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var glob = require('glob'); 5 | var rjs = /\.js$/i; 6 | var rindex = /\/index\.js$/i; 7 | var models; 8 | 9 | function load (pattern) { 10 | var modules = glob.sync(pattern); 11 | return modules.filter(notIndex).map(unwrap).reduce(keys, models); 12 | 13 | function keys (accumulator, model, i) { 14 | var name = path.basename(modules[i], '.js'); 15 | accumulator[name] = model; 16 | return accumulator; 17 | } 18 | 19 | function notIndex (file) { 20 | return rjs.test(file) && rindex.test(file) === false; 21 | } 22 | } 23 | 24 | function unwrap (file) { 25 | return require(path.join(__dirname, file)); 26 | } 27 | 28 | function models (paths) { 29 | paths.forEach(load); 30 | return models; 31 | } 32 | 33 | module.exports = models; 34 | -------------------------------------------------------------------------------- /mongotape.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tape = require('tape'); 4 | var contra = require('contra'); 5 | var state = require('./state'); 6 | var connection = require('./connection'); 7 | var drop = require('./drop'); 8 | 9 | function mongotape (description, tests) { 10 | var desc = tests ? description : 'mongotape test bundle'; 11 | var fn = mongotaped.bind(null, tests || description); 12 | tape.test(desc, fn); 13 | } 14 | 15 | function mongotaped (tests, st) { 16 | var models = state.models(); 17 | var sub = subtest.bind(null, models, st); 18 | sub.skip = skip; 19 | tests(sub); 20 | st.end(); 21 | } 22 | 23 | function subtest (models, st, description, fn) { 24 | if (!fn) { 25 | fn = description; 26 | description = null; 27 | } 28 | 29 | st.test(description + '::connect', connect); 30 | st.test(description, fn); 31 | st.test(description + '::disconnect', disconnect); 32 | 33 | function connect (t) { 34 | contra.series([contra.curry(connection.connect), contra.curry(drop), ensureModels], t.end); 35 | } 36 | 37 | function ensureModels (done) { 38 | contra.each(models, ensureIndexes, done); 39 | } 40 | 41 | function ensureIndexes (model, next) { 42 | model.ensureIndexes(next); 43 | } 44 | 45 | function disconnect (t) { 46 | contra.series([contra.curry(connection.disconnect)], t.end); 47 | } 48 | } 49 | 50 | function skip () {} 51 | 52 | mongotape.skip = skip; 53 | module.exports = mongotape; 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongotape", 3 | "version": "1.0.0", 4 | "description": "Run integration tests using mongoose and tape", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/bevacqua/mongotape.git" 9 | }, 10 | "author": "Nicolas Bevacqua (http://bevacqua.io/)", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/bevacqua/mongotape/issues" 14 | }, 15 | "homepage": "https://github.com/bevacqua/mongotape", 16 | "scripts": { 17 | "test": "node test/*" 18 | }, 19 | "dependencies": { 20 | "contra": "1.9.1", 21 | "glob": "5.0.13" 22 | }, 23 | "devDependencies": { 24 | "proxyquire": "1.6.0", 25 | "sinon": "1.15.4", 26 | "tape": "4.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # mongotape 2 | 3 | > Run integration tests using `mongoose` and `tape` 4 | 5 | # install 6 | 7 | ```shell 8 | npm i mongotape -S 9 | ``` 10 | 11 | # usage 12 | 13 | Set up a `./mongotape` module as follows. 14 | 15 | ```js 16 | var mongotape = require('mongotape'); 17 | var options = { 18 | mongoose: require('mongoose'), 19 | models: 'models/*' 20 | }; 21 | module.exports = mongotape(options); 22 | ``` 23 | 24 | You're all set, your tests should now use that module to access the `mongotape` API. 25 | 26 | # `mongotape(description?, tests)` 27 | 28 | The `tests` callback will receive a `test` object that matches the API you would expect from `tape`. The difference is that each test will include a setup and teardown. 29 | 30 | ### setup 31 | 32 | - Database connection is established 33 | - Database is dropped 34 | - Database is re-created and models are re-loaded 35 | 36 | ### teardown 37 | 38 | - Database connection is closed 39 | 40 | This process ensures each test runs effectively in isolation, which is usually a very painful thing to achieve using plain `tape` and `mongoose`. 41 | 42 | ### example 43 | 44 | ```js 45 | mongotape(function tests (test) { 46 | test('.recent pulls recent sorted by creation date, descending', function (t) { 47 | contra.series([ 48 | function (next) { 49 | new models.Log({ level: 'info', message: 'some foo' }).save(next); 50 | }, 51 | function (next) { 52 | new models.Log({ level: 'error', message: 'some bar' }).save(next); 53 | }, 54 | function (next) { 55 | logQueryService.recent({}, next); 56 | } 57 | ], function (err, results) { 58 | var logs = results.pop(); 59 | t.equal(err, null); 60 | t.equal(logs.length, 2); 61 | t.equal(logs[0].message, 'some bar'); 62 | t.equal(logs[1].message, 'some foo'); 63 | t.end(); 64 | }); 65 | }); 66 | }); 67 | ``` 68 | 69 | You can mix `mongotape` and _regular Tape's_ `test` statements as you see fit. There's no need to call a special method to signal that you are done, as mongotape will simply yield execution once all your mongoose `test` handlers have ran to completion. 70 | 71 | # options 72 | 73 | You can set a few options when first configuring `mongotape` as [seen above](#usage). 74 | 75 | #### mongoose 76 | 77 | Set `mongoose` to your local mongoose instance so that we have the same exact version 78 | 79 | #### models 80 | 81 | Set `models` to the path to your mongoose models. It can be an string, an array, or a method that returns the models in an object like: 82 | 83 | ```js 84 | { 85 | Article: ArticleModel, 86 | Comment: CommentModel 87 | } 88 | ``` 89 | 90 | _(and so on...)_ 91 | 92 | #### env 93 | 94 | If you use an environment variables manager such as `nconf`, set `options.env` to a method that returns an environment configuration value given a key. Here's the default implementation: 95 | 96 | ```js 97 | function env (key) { 98 | return process.env[key]; 99 | } 100 | ``` 101 | # license 102 | 103 | MIT 104 | -------------------------------------------------------------------------------- /state.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var loader = require('./model-loader'); 4 | var state = { 5 | env: env, 6 | models: models, 7 | configure: configure 8 | }; 9 | 10 | function env (key) { 11 | return process.env[key]; 12 | } 13 | 14 | function models () { 15 | throw new Error('You must define a `models` function that loads your Mongoose models!\n\nmongotape({\n models: models\n})'); 16 | } 17 | 18 | function loads (paths) { 19 | return function load () { 20 | return loader(paths); 21 | }; 22 | } 23 | 24 | function configure (options) { 25 | if (typeof options.models === 'string') { 26 | state.models = loads([options.models]); 27 | } else if (Array.isArray(options.models)){ 28 | state.models = loads(options.models); 29 | } else { 30 | state.models = options.models; 31 | } 32 | if (options.env) { state.env = options.env; } 33 | if (options.mongoose) { state.mongoose = options.mongoose; } 34 | } 35 | 36 | module.exports = state; 37 | -------------------------------------------------------------------------------- /test/drop.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var sinon = require('sinon'); 5 | var proxyquire = require('proxyquire'); 6 | var mongoose; 7 | 8 | function env (config) { 9 | return function get (key) { 10 | return config[key]; 11 | }; 12 | } 13 | 14 | function mock (config) { 15 | mongoose = { 16 | connection: { db: { dropDatabase: sinon.spy() } } 17 | }; 18 | return proxyquire('../drop', { 19 | './state': { 20 | mongoose: mongoose, 21 | env: env(config) 22 | } 23 | }); 24 | } 25 | 26 | test('exposes expected API', function (t) { 27 | var dbService = mock({}); 28 | t.equal(Object.keys(dbService).length, 1, 'exposes correct API member count'); 29 | t.ok(typeof dbService.drop === 'function', 'exposes .drop method'); 30 | t.end(); 31 | }); 32 | 33 | test('.drop fails unless test environment on NODE_ENV', function (t) { 34 | t.throws(function () { 35 | mock({ NODE_ENV: 'production' }).drop(sinon.spy()); 36 | }); 37 | t.throws(function () { 38 | mock({ NODE_ENV: 'staging' }).drop(sinon.spy()); 39 | }); 40 | t.throws(function () { 41 | mock({ NODE_ENV: 'staging-two' }).drop(sinon.spy()); 42 | }); 43 | t.throws(function () { 44 | mock({ NODE_ENV: 'development' }).drop(sinon.spy()); 45 | }); 46 | t.end(); 47 | }); 48 | 49 | test('.drop fails unless test somewhere in MONGO_URI', function (t) { 50 | t.throws(function () { 51 | mock({ NODE_ENV: 'test' }).drop(sinon.spy()); 52 | }); 53 | t.throws(function () { 54 | mock({ NODE_ENV: 'test', MONGO_URI: 'mongodb://localhost/stompflow' }).drop(sinon.spy()); 55 | }); 56 | t.doesNotThrow(function () { 57 | mock({ NODE_ENV: 'test', MONGO_URI: 'mongodb://localhost/stompflow-test' }).drop(sinon.spy()); 58 | }); 59 | t.end(); 60 | }); 61 | 62 | test('.drop drops the database when successfully validated', function (t) { 63 | var done = sinon.spy(); 64 | mock({ NODE_ENV: 'test', MONGO_URI: 'mongodb://localhost/stompflow-test' }).drop(done); 65 | t.equal(mongoose.connection.db.dropDatabase.callCount, 1); 66 | t.deepEqual(mongoose.connection.db.dropDatabase.firstCall.args, [done]); 67 | t.end(); 68 | }); 69 | --------------------------------------------------------------------------------