├── .gitignore ├── .travis.yml ├── lib ├── .jshintrc └── injectr.js ├── test ├── .jshintrc ├── integration.js └── spec.js ├── LICENSE ├── Gruntfile.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | *~ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | - "0.8" 6 | before_script: 7 | - npm install -g grunt-cli 8 | -------------------------------------------------------------------------------- /lib/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals" : { 3 | "window" : false, 4 | "document" : false 5 | }, 6 | "camelcase" : true, 7 | "curly" : true, 8 | "eqeqeq" : true, 9 | "forin" : true, 10 | "freeze" : true, 11 | "immed" : true, 12 | "indent" : 4, 13 | "latedef" : "nofunc", 14 | "newcap" : true, 15 | "noarg" : true, 16 | "node" : true, 17 | "nomen" : true, 18 | "undef" : true, 19 | "unused" : true, 20 | "trailing" : true, 21 | "maxlen" : 80 22 | } -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals" : { 3 | "describe" : false, 4 | "it" : false, 5 | "beforeEach" : false, 6 | "afterEach" : false, 7 | "before" : false, 8 | "after" : false 9 | }, 10 | "camelcase" : true, 11 | "curly" : true, 12 | "eqeqeq" : true, 13 | "forin" : true, 14 | "freeze" : true, 15 | "immed" : true, 16 | "indent" : 4, 17 | "latedef" : "nofunc", 18 | "newcap" : true, 19 | "noarg" : true, 20 | "node" : true, 21 | "nomen" : true, 22 | "undef" : true, 23 | "unused" : true, 24 | "trailing" : true, 25 | "maxlen" : 120 26 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Nathan MacInnes 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /lib/injectr.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var cache = {}, 4 | fs = require('fs'), 5 | path = require('path'), 6 | vm = require('vm'); 7 | 8 | module.exports = function (file, mocks, context) { 9 | var script; 10 | mocks = mocks || {}; 11 | context = context || {}; 12 | file = path.join(path.dirname(module.parent.filename), file); 13 | cache[file] = cache[file] || module.exports.onload(file, 14 | fs.readFileSync(file, 'utf8')); 15 | script = vm.createScript(cache[file], file); 16 | context.require = function (a) { 17 | if (mocks[a]) { 18 | return mocks[a]; 19 | } 20 | if (a.indexOf('.') === 0) { 21 | a = path.join(path.dirname(file), a); 22 | } 23 | return require(a); 24 | }; 25 | context.module = context.module || {}; 26 | context.module.exports = {}; 27 | context.exports = context.module.exports; 28 | 29 | script.runInNewContext(context); 30 | return context.module.exports; 31 | }; 32 | 33 | module.exports.onload = function (file, content) { 34 | if (file.match(/\.coffee$/)) { 35 | return require('coffee-script').compile(content, { 36 | filename : file 37 | }); 38 | } 39 | return content; 40 | }; 41 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | mochaTest: { 7 | test: { 8 | options: { 9 | reporter: 'spec' 10 | }, 11 | src: ['test/*.js'] 12 | } 13 | }, 14 | jshint : { 15 | core : ['Gruntfile.js'], 16 | test : ['test/*.js'], 17 | lib : ['lib/*.js'], 18 | options : { 19 | jshintrc : true 20 | } 21 | }, 22 | watch : { 23 | lib : { 24 | files : ['lib/*.js'], 25 | tasks : ['jshint:lib', 'test'] 26 | }, 27 | test : { 28 | files : ['test/*.js'], 29 | tasks : ['jshint:test', 'test'] 30 | }, 31 | grunt : { 32 | files : ['Gruntfile.js'], 33 | tasks : ['jshint:core'] 34 | } 35 | } 36 | }); 37 | 38 | grunt.loadNpmTasks('grunt-contrib-jshint'); 39 | grunt.loadNpmTasks('grunt-mocha-test'); 40 | 41 | grunt.registerTask('test', 'mochaTest'); 42 | grunt.registerTask('lint', 'jshint'); 43 | 44 | grunt.registerTask('default', ['lint', 'test']); 45 | 46 | grunt.loadNpmTasks('grunt-contrib-watch'); 47 | }; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "injectr", 3 | "version": "0.5.1", 4 | "description": "Finally, a solution to node.js dependency injection", 5 | "keywords": [ 6 | "dependency injection", 7 | "mock", 8 | "unit", 9 | "test" 10 | ], 11 | "author": { 12 | "name": "Nathan MacInnes", 13 | "email": "nathan@macinn.es", 14 | "web": "http://macinn.es" 15 | }, 16 | "main": "lib/injectr.js", 17 | "bugs": { 18 | "email": "nathan@macinn.es", 19 | "url": "https://github.com/nathanmacinnes/injectr/issues" 20 | }, 21 | "licenses": { 22 | "type": "MIT", 23 | "url": "http: //www.opensource.org/licenses/mit-license.php" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/nathanmacinnes/injectr" 28 | }, 29 | "engines": { 30 | "node": ">=0.6.x" 31 | }, 32 | "devDependencies": { 33 | "expect.js": "0.1.0 - 0.3.x", 34 | "grunt": "^0.4.4", 35 | "grunt-contrib-jshint": "^0.10.0", 36 | "grunt-contrib-watch": "^0.6.1", 37 | "grunt-mocha-test": "^0.10.2", 38 | "jshint": "~2.5", 39 | "mocha": "^1.18.2", 40 | "pretendr": "~0.5" 41 | }, 42 | "scripts": { 43 | "test": "grunt test", 44 | "lint": "grunt lint" 45 | }, 46 | "directories": { 47 | "lib": "lib", 48 | "test": "test" 49 | }, 50 | "files": [ 51 | "Makefile", 52 | "README.md", 53 | "LICENSE", 54 | "lib/", 55 | "test/" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # injectr # 2 | 3 | _Finally, a solution to node.js dependency injection_ 4 | 5 | ## Install it ## 6 | 7 | `npm install injectr`. Boom. 8 | 9 | ## Use it ## 10 | 11 | var injectr = require('injectr'); 12 | var myScript = injectr('../lib/myScript.js', { 13 | fs : mockFs, 14 | crypto : mockCrypto 15 | }); 16 | 17 | Now when you `require('fs')` or `require('crypto')` in myScript.js, what you 18 | get is `mockFs` or `mockCrypto`. 19 | 20 | Treat **injectr** like `require` for your tests, with a second argument to pass 21 | in your mocks. 22 | 23 | **Paths are now relative to the current file, just like require.** Please update 24 | your tests if you are upgrading from v0.4 or below. 25 | 26 | ### Context ### 27 | 28 | **injectr** gives you access to the context of the **injectr**'d file via 29 | an optional third argument. Provide an object, and **injectr** will modify it 30 | as necessary and use that as the context. 31 | 32 | var myScript = injectr('../lib/myScript.js', {}, { 33 | Date : mockDate, 34 | setTimeout : mockSetTimeout 35 | }); 36 | 37 | As of version 0.4, **injectr** doesn't create a full node.js context for you to 38 | use. Instead, it isolates your script in its own sandbox, allowing you to 39 | include mocks of only the bits that your script needs. 40 | 41 | ### CoffeeScript ### 42 | 43 | **injectr** compiles any *.coffee files for you, so you can test your 44 | CoffeeScript too. The default settings can be changed by overwriting the 45 | `injectr.onload` function. It takes the filename and file contents as 46 | arguments, and returns the compiled script. 47 | 48 | ## Share it ## 49 | 50 | **injectr** is under the [MIT License](http://www.opensource.org/licenses/MIT). 51 | [Fork it](https://github.com/nathanmacinnes/injectr). Modify it. Pass it around. 52 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var expect = require("expect.js"), 4 | fs = require('fs'); 5 | 6 | describe("basic injectr", function () { 7 | beforeEach(function (done) { 8 | var fileContent, 9 | self = this; 10 | this.testFileDirectory = __dirname + '/test-scripts/'; 11 | this.testFile = this.testFileDirectory + 'test-file.js'; 12 | this.relativeTestFile = './test-scripts/test-file.js'; 13 | this.testRequire = 'test-require.js'; 14 | this.testRequire2 = '../test-require2.js'; 15 | this.injectr = require('../lib/injectr'); 16 | fileContent = 'module.exports = {' + 17 | ' a : "result",' + 18 | ' b : function () {' + 19 | ' return require("fs");' + 20 | ' },' + 21 | ' c : function () {' + 22 | ' return require("./' + this.testRequire + '");' + 23 | ' },' + 24 | ' d : function () {' + 25 | ' return require("' + this.testRequire2 + '");' + 26 | ' }' + 27 | '};'; 28 | fs.exists(this.testFileDirectory, function (exists) { 29 | var testFilesToDo = 3, 30 | write; 31 | write = function () { 32 | var complete = function () { 33 | if (--testFilesToDo === 0) { 34 | done(); 35 | } 36 | }; 37 | fs.writeFile( 38 | self.testFile, 39 | fileContent, 40 | complete 41 | ); 42 | fs.writeFile( 43 | self.testFileDirectory + self.testRequire, 44 | 'module.exports = 4;', 45 | complete 46 | ); 47 | fs.writeFile( 48 | self.testFileDirectory + self.testRequire2, 49 | 'module.exports = 5;', 50 | complete 51 | ); 52 | }; 53 | if (!exists) { 54 | fs.mkdir(self.testFileDirectory, write); 55 | } else { 56 | write(); 57 | } 58 | }); 59 | }); 60 | afterEach(function (done) { 61 | var self = this, 62 | testFilesToDo = 2, 63 | complete = function () { 64 | if (--testFilesToDo === 0) { 65 | fs.rmdir(self.testFileDirectory, done); 66 | } 67 | }; 68 | fs.unlink(this.testFile, complete); 69 | fs.unlink(self.testFileDirectory + self.testRequire, complete); 70 | fs.unlink(self.testFileDirectory + self.testRequire2, complete); 71 | }); 72 | it("should load and run scripts, and return the result", function () { 73 | var mod = this.injectr(this.relativeTestFile); 74 | expect(mod).to.have.property('a', 'result'); 75 | }); 76 | it("should run scripts replacing mocks with passed objects", function () { 77 | var mod, 78 | mockFs = {}; 79 | mod = this.injectr(this.relativeTestFile, { 80 | fs : mockFs 81 | }); 82 | expect(mod.b()).to.equal(mockFs); 83 | }); 84 | it("should successfully resolve dirs to the mocking file", function () { 85 | var mod; 86 | mod = this.injectr(this.relativeTestFile, { 87 | }); 88 | expect(mod.c()).to.equal(4); 89 | }); 90 | it("should successfully resolve dirs to a ../ file", function () { 91 | var mod; 92 | mod = this.injectr(this.relativeTestFile, { 93 | }); 94 | expect(mod.d()).to.equal(5); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var expect = require("expect.js"), 4 | injectr = require("../lib/injectr"), 5 | pretendr = require("pretendr"); 6 | 7 | describe("injectr", function () { 8 | beforeEach(function () { 9 | this.mockFs = pretendr({ 10 | readFileSync : function () {} 11 | }); 12 | this.mockPath = pretendr({ 13 | join : function () {}, 14 | dirname : function () {} 15 | }); 16 | this.mockPath.join.returnValue('dir/filename'); 17 | this.mockVm = pretendr({ 18 | createScript : function () {} 19 | }); 20 | this.mockVm.createScript.template({ 21 | runInNewContext : function () {}, 22 | runInThisContext : function () {} 23 | }); 24 | this.mockCoffeeScript = pretendr({ 25 | compile : function () {} 26 | }); 27 | 28 | // this.injectr is the injectr under test 29 | // the injectr var is being used to test it 30 | this.injectr = injectr('../lib/injectr.js', { 31 | fs : this.mockFs.mock, 32 | path : this.mockPath.mock, 33 | vm : this.mockVm.mock, 34 | 'coffee-script' : this.mockCoffeeScript.mock 35 | }, { 36 | module : { 37 | parent : module 38 | } 39 | }); 40 | }); 41 | it("should attempt to resolve the selected file", function () { 42 | this.mockPath.dirname.returnValue('dirname'); 43 | this.injectr('./filename'); 44 | expect(this.mockPath.dirname.calls).to.have.length(1); 45 | expect(this.mockPath.dirname.calls[0].args) 46 | .to.have.property(0, __filename); 47 | expect(this.mockPath.join.calls).to.have.length(1); 48 | expect(this.mockPath.join.calls[0].args) 49 | .to.have.property(0, 'dirname') 50 | .and.to.have.property(1, './filename'); 51 | }); 52 | it("should read in the selected file", function () { 53 | this.injectr('filename'); 54 | expect(this.mockFs.readFileSync.calls).to.have.length(1); 55 | expect(this.mockFs.readFileSync.calls[0].args) 56 | .to.have.property(0, 'dir/filename') 57 | .and.to.have.property(1, 'utf8'); 58 | }); 59 | it("should only read the file once per file", function () { 60 | this.mockFs.readFileSync.returnValue('dummy'); 61 | this.injectr('filename'); 62 | this.injectr('filename'); 63 | expect(this.mockFs.readFileSync.calls).to.have.length(1); 64 | this.mockPath.join.returnValue('dir/filename2'); 65 | this.injectr('filename2'); 66 | this.injectr('filename2'); 67 | expect(this.mockFs.readFileSync.calls).to.have.length(2); 68 | }); 69 | it("should create a script from the file", function () { 70 | var call; 71 | this.mockFs.readFileSync.returnValue('dummy script'); 72 | this.injectr('filename'); 73 | expect(this.mockVm.createScript.calls).to.have.length(1); 74 | call = this.mockVm.createScript.calls[0]; 75 | expect(call.args[0]).to.equal('dummy script'); 76 | expect(call.args[1]).to.equal('dir/filename'); 77 | }); 78 | it("should run the script in a new context", function () { 79 | var mockScript; 80 | this.injectr('filename'); 81 | mockScript = this.mockVm.createScript.calls[0].pretendr; 82 | expect(mockScript.runInNewContext.calls).to.have.length(1); 83 | }); 84 | it("should have a predefined module.exports", function () { 85 | var context, 86 | mockScript; 87 | this.injectr('filename'); 88 | mockScript = this.mockVm.createScript.calls[0].pretendr; 89 | context = mockScript.runInNewContext.calls[0].args[0]; 90 | expect(context).to.have.property('module'); 91 | expect(context.module).to.have.property('exports'); 92 | expect(context.module.exports).to.be.an('object'); 93 | }); 94 | it("should have an exports property equal to module.exports", function () { 95 | var context, 96 | mockScript; 97 | this.injectr('filename'); 98 | mockScript = this.mockVm.createScript.calls[0].pretendr; 99 | context = mockScript.runInNewContext.calls[0].args[0]; 100 | expect(context).to.have.property('exports', context.module.exports); 101 | }); 102 | it("should return module.exports", function () { 103 | var context, 104 | mockScript, 105 | l; 106 | l = this.injectr('filename'); 107 | mockScript = this.mockVm.createScript.calls[0].pretendr; 108 | context = mockScript.runInNewContext.calls[0].args[0]; 109 | expect(l).to.equal(context.module.exports); 110 | }); 111 | it("should allow an onload callback", function () { 112 | var mockCb = pretendr(function () {}); 113 | this.mockFs.readFileSync.returnValue('before'); 114 | mockCb.returnValue('after'); 115 | this.injectr.onload = mockCb.mock; 116 | this.injectr('filename'); 117 | expect(mockCb.calls).to.have.length(1); 118 | expect(mockCb.calls[0].args).to.have.property(0, 'dir/filename') 119 | .and.to.have.property(1, 'before'); 120 | expect(this.mockVm.createScript.calls[0].args[0]).to.equal('after'); 121 | }); 122 | it("should only run the callback once per file", function () { 123 | var mockCb = pretendr(function () {}); 124 | mockCb.returnValue('dummy'); 125 | this.injectr.onload = mockCb.mock; 126 | this.injectr('filename'); 127 | this.injectr('filename'); 128 | expect(mockCb.calls).to.have.length(1); 129 | }); 130 | it("should use the provided context to run the script", function () { 131 | var context = {}, 132 | mockScript; 133 | this.injectr('filename', null, context); 134 | mockScript = this.mockVm.createScript.calls[0].pretendr; 135 | expect(mockScript.runInNewContext.calls[0].args[0]) 136 | .to.equal(context); 137 | }); 138 | it("should still have module and require objects", function () { 139 | var context = {}; 140 | this.injectr('filename', null, context); 141 | expect(context).to.have.property('module') 142 | .and.to.have.property('require'); 143 | }); 144 | describe("#require", function () { 145 | it("should get mock libraries if provided", function () { 146 | var context, 147 | customLib = {}, 148 | mockScript; 149 | this.injectr('filename', { 150 | customLib : customLib 151 | }); 152 | mockScript = this.mockVm.createScript.calls[0].pretendr; 153 | context = mockScript.runInNewContext.calls[0].args[0]; 154 | expect(context.require('customLib')).to.equal(customLib); 155 | }); 156 | it("should require libraries otherwise", function () { 157 | var context, 158 | mockScript; 159 | this.injectr('filename', {}); 160 | mockScript = this.mockVm.createScript.calls[0].pretendr; 161 | context = mockScript.runInNewContext.calls[0].args[0]; 162 | expect(JSON.stringify(context.require('fs'))) 163 | .to.eql(JSON.stringify(require('fs'))); 164 | }); 165 | it("should resolve directories to the injectr'd file", function () { 166 | var args, 167 | l, 168 | mockScript, 169 | req; 170 | l = this.injectr('filename'); 171 | mockScript = this.mockVm.createScript.calls[0].pretendr; 172 | req = mockScript.runInNewContext.calls[0].args[0].require; 173 | this.mockPath.dirname.returnValue('/directory/'); 174 | this.mockPath.join.returnValue('fs'); 175 | req('./a-local-module'); 176 | args = this.mockPath.dirname.calls[1].args; 177 | expect(args).to.have.property(0, 'dir/filename'); 178 | args = this.mockPath.join.calls[1].args; 179 | expect(args[0]).to.equal('/directory/'); 180 | expect(args[1]).to.equal('./a-local-module'); 181 | expect(JSON.stringify(l)).to.equal(JSON.stringify(require('fs'))); 182 | }); 183 | it("should compile coffee-script files before running", function () { 184 | var out; 185 | this.mockCoffeeScript.compile.returnValue('compiled'); 186 | out = this.injectr.onload('file.coffee', 'uncompiled'); 187 | expect(out).to.equal('compiled'); 188 | this.mockCoffeeScript.compile.returnValue('another compiled'); 189 | out = this.injectr.onload('another.coffee', ''); 190 | expect(out).to.equal('another compiled'); 191 | out = this.injectr.onload('non-coffee', 'javascript'); 192 | expect(out).to.equal('javascript'); 193 | }); 194 | }); 195 | }); 196 | --------------------------------------------------------------------------------