├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── Jakefile ├── LICENSE ├── README.md ├── TODO.md ├── example └── mocking-fs │ ├── app.js │ └── app.spec.coffee ├── jsl.conf ├── lib ├── chokidar.js ├── fs.js ├── glob.js ├── http.js ├── index.js └── util.js ├── package.json └── test ├── chokidar.spec.coffee ├── fixtures ├── other.js └── some.js ├── fs.spec.coffee ├── glob.spec.coffee ├── http.spec.coffee └── util.spec.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ./.git/ 2 | ./node_modules/ 3 | ./.idea/ 4 | ./test/ 5 | 6 | .gitignore 7 | .npmignore 8 | pom.xml 9 | TODO.md 10 | jsl.conf 11 | Jakefile 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.6 4 | 5 | script: 6 | - jasmine-node --coffee test 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v0.0.15 2 | * feat(fs): support relative paths 3 | * fix(util.loadFile): do not override the exceptions 4 | 5 | ### v0.0.14 6 | * http.ServerResponse: publish headerSent, getHeader, write 7 | * http.ServerRequest: add getHeader 8 | 9 | ### v0.0.13 10 | * Add http.ServerRequest.headers 11 | 12 | ### v0.0.12 13 | * http.ServerResponse emit "end" event 14 | 15 | ### v0.0.11 16 | * Added exists/existsSync to fs mocks 17 | 18 | ### v0.0.10 19 | * Make http.ServerResponse and http.ServerRequest event emitters 20 | 21 | ### v0.0.9 22 | * Normalize path separator 23 | 24 | ### v0.0.8 25 | * Add fs.mkdirSync() 26 | * Add fs.readdirSync() 27 | 28 | ### v0.0.7 29 | * Allow injecting mocks into nested deps 30 | 31 | ### v0.0.6 32 | * Add fs.mkdir() 33 | * Add fs.statSync() 34 | * Add fs.writeFile() 35 | 36 | ### v0.0.5 37 | * Trivial implementation of chokidar 38 | * Trivial mock for node-glob 39 | * Improve predictableNextTick to follow pattern even for nested callbacks 40 | 41 | ### v0.0.4 42 | Set error code on exceptions 43 | 44 | ### v0.0.3 45 | * Correct stack trace when syntax error during loadFile 46 | * Add missing globals to util.loadFile 47 | 48 | ### v0.0.2 49 | * Update loadFile - allow passing globals 50 | 51 | ### v0.0.1 52 | * Initial version, extracted from [SlimJim] 53 | 54 | [SlimJim]: https://github.com/vojtajina/slim-jim/ 55 | -------------------------------------------------------------------------------- /Jakefile: -------------------------------------------------------------------------------- 1 | desc('Run unit tests.'); 2 | task('test', function() { 3 | console.log('Running unit tests...'); 4 | jake.exec(['jasmine-node --coffee test'], complete, {stdout: true}); 5 | }); 6 | 7 | 8 | desc('Bump minor version, update changelog, create tag, push to github.'); 9 | task('version', function () { 10 | var fs = require('fs'); 11 | 12 | var packagePath = process.cwd() + '/package.json'; 13 | var pkg = JSON.parse(fs.readFileSync(packagePath).toString()); 14 | var versionArray = pkg.version.split('.'); 15 | var previousVersionTag = 'v' + pkg.version; 16 | 17 | // bump minor version 18 | versionArray.push(parseInt(versionArray.pop(), 10) + 1); 19 | pkg.version = versionArray.join('.'); 20 | 21 | // Update package.json with the new version-info 22 | fs.writeFileSync(packagePath, JSON.stringify(pkg, true, 2)); 23 | 24 | var TEMP_FILE = '.changelog.temp'; 25 | var message = 'Bump version to v' + pkg.version; 26 | jake.exec([ 27 | // update changelog 28 | 'echo "### v' + pkg.version + '" > ' + TEMP_FILE, 29 | 'git log --pretty="* %s" ' + previousVersionTag + '..HEAD >> ' + TEMP_FILE, 30 | 'echo "" >> ' + TEMP_FILE, 31 | 'cat CHANGELOG.md >> ' + TEMP_FILE, 32 | 'mv ' + TEMP_FILE + ' CHANGELOG.md', 33 | 'sublime -w CHANGELOG.md', 34 | 35 | // commit + push to github 36 | 'git commit package.json CHANGELOG.md -m "' + message + '"', 37 | 'git push origin master', 38 | 'git tag -a v' + pkg.version + ' -m "Version ' + pkg.version + '"', 39 | 'git push --tags' 40 | ], function () { 41 | console.log(message); 42 | complete(); 43 | }); 44 | }); 45 | 46 | 47 | desc('Bump version, publish to npm.'); 48 | task('publish', ['version'], function() { 49 | jake.exec([ 50 | 'npm publish' 51 | ], function() { 52 | console.log('Published to npm'); 53 | complete(); 54 | }); 55 | }); 56 | 57 | 58 | desc('Run JSLint check.'); 59 | task('jsl', function() { 60 | jake.exec(['jsl -conf jsl.conf'], complete, {stdout: true}); 61 | }); 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (C) 2012-2013 Vojta Jína. 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 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Mocks [![Build Status](https://secure.travis-ci.org/vojtajina/node-mocks.png?branch=master)](http://travis-ci.org/vojtajina/node-mocks) 2 | 3 | Set of mocks and utilities for easier unit testing with [Node.js]. 4 | 5 | See http://howtonode.org/testing-private-state-and-mocking-deps for better explanation. 6 | 7 | ## Prerequisites 8 | 9 | * [Node.js] 10 | * [NPM] (shipped with Node since 0.6.3) 11 | 12 | 13 | ## Installation 14 | 15 | sudo npm install -g mocks 16 | 17 | # or install in local folder 18 | npm install mocks 19 | 20 | 21 | ## Example 22 | 23 | ### Mocking a file system 24 | 25 | ````javascript 26 | // during unit test - we inject mock file system instead of real fs 27 | var fs = require('fs'); 28 | 29 | // even if this function is not public, we can test it directly 30 | var filterOnlyExistingFiles = function(collection, done) { 31 | var filtered = [], 32 | waiting = 0; 33 | 34 | collection.forEach(function(file) { 35 | waiting++; 36 | fs.stat(file, function(err, stat) { 37 | if (!err && stat.isFile()) filtered.push(file); 38 | waiting--; 39 | if (!waiting) done(null, filtered); 40 | }); 41 | }); 42 | }; 43 | ```` 44 | 45 | ````coffeescript 46 | # simple unit test (jasmine syntax, coffescript) 47 | describe 'app', -> 48 | mocks = require 'mocks' 49 | loadFile = mocks.loadFile 50 | app = done = null 51 | 52 | beforeEach -> 53 | done = jasmine.createSpy 'done' 54 | # load the file and inject fake fs module 55 | app = loadFile __dirname + '/app.js', 56 | fs: mocks.fs.create 57 | 'bin': 58 | 'run.sh': 1, 59 | 'install.sh': 1 60 | 'home': 61 | 'some.js': 1, 62 | 'another.txt': 1 63 | 'one.js': 1, 64 | 'two.js': 1, 65 | 'three.js': 1 66 | 67 | 68 | it 'should return only existing files', -> 69 | done.andCallFake (err, filtered) -> 70 | expect(err).toBeFalsy() 71 | expect(filtered).toEqual ['/bin/run.sh'] 72 | 73 | app.filterOnlyExistingFiles ['/bin/run.sh', '/non.txt'], done 74 | waitsFor -> done.callCount 75 | 76 | it 'should ignore directories', -> 77 | done.andCallFake (err, filtered) -> 78 | expect(filtered).toEqual ['/bin/run.sh', '/home/some.js'] 79 | 80 | app.filterOnlyExistingFiles ['/bin/run.sh', '/home', '/home/some.js'], done 81 | waitsFor -> done.callCount 82 | ```` 83 | 84 | ### Faking randomness 85 | Non-blocking I/O operations can return in random order. Let's say you read a content of two files (asynchronously). There is no guarantee, that you get the content in right order. That's fine, but we want to test our code, whether it can handle such a situation and still work properly. In that case, you can use `predictableNextTick`, which process callbacks depending on given pattern. 86 | 87 | ````coffeescript 88 | 89 | it 'should preserve order', -> 90 | done.andCallFake (err, filtered) -> 91 | expect(filtered).toEqual ['/one.js', '/two.js', '/three.js'] 92 | 93 | app.filterOnlyExistingFiles ['/one.js', '/two.js', '/three.js'], done 94 | waitsFor -> done.callCount 95 | ```` 96 | This test will always pass. That's cool, as we like to see tests passing. The bad thing is, that it does not work in production, with real file system, as it might return in different order... 97 | So, we need to test, whether our app works even when the `fs` returns in random order. Having randomness in unit tests is not good habit, as it leads to flaky tests. 98 | Let's change the previous unit test to this: 99 | 100 | ````coffeescript 101 | it 'should preserve order', -> 102 | done.andCallFake (err, filtered) -> 103 | expect(filtered).toEqual ['/one.js', '/two.js', '/three.js'] 104 | 105 | mocks.predictableNextTick.pattern = [2, 0, 1] 106 | app.filterOnlyExistingFiles ['/one.js', '/two.js', '/three.js'], done 107 | waitsFor -> done.callCount 108 | ```` 109 | Now, the unit test fails, because our fake file system calls back in different order. Note, it's not random, as you explicitly specified the pattern (2, 0, 1), so it the fake fs will consistently call back in this order: /three.js, /one.js, two.js. 110 | 111 | 112 | ## API 113 | Currently supported API is only very small part of real [Node's API]. Basically I only implemented methods I need for testing [Testacular]. 114 | 115 | I will keep adding more and of course if anyone wants to help - pull requests are more than welcomed. 116 | 117 | ### [fs](http://nodejs.org/api/fs.html) 118 | 119 | - stat(path, callback) 120 | - readdir(path, callback) 121 | - readFile(path [, encoding], callback) 122 | - readFileSync(path) 123 | - watchFile(path [, options], callback) 124 | - _touchFile(path, mtime, content) * 125 | 126 | ### [http](http://nodejs.org/api/http.html) 127 | 128 | - http.ServerResponse 129 | - http.ServerRequest 130 | 131 | ### [glob](https://github.com/isaacs/node-glob) 132 | 133 | 134 | ### loadFile(path [, mocks] [, globals]) 135 | ### predictableNextTick(fn) 136 | ### predictableNextTick.pattern 137 | 138 | [Node.js]: http://nodejs.org/ 139 | [NPM]: http://npmjs.org/ 140 | [Node's API]: http://nodejs.org/docs/latest/api/index.html 141 | [Testacular]: https://github.com/vojtajina/testacular 142 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vojtajina/node-mocks/e42ab03853bd68922db13d66330f722c27e67591/TODO.md -------------------------------------------------------------------------------- /example/mocking-fs/app.js: -------------------------------------------------------------------------------- 1 | // during unit test - we inject mock file system instead of real fs 2 | var fs = require('fs'); 3 | 4 | // even if this function is not public, we can test it directly 5 | var filterOnlyExistingFiles = function(collection, done) { 6 | var filtered = [], 7 | waiting = 0; 8 | 9 | collection.forEach(function(file) { 10 | waiting++; 11 | fs.stat(file, function(err, stat) { 12 | if (!err && stat.isFile()) filtered.push(file); 13 | waiting--; 14 | if (!waiting) done(null, filtered); 15 | }); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /example/mocking-fs/app.spec.coffee: -------------------------------------------------------------------------------- 1 | # simple unit test (jasmine syntax, coffescript) 2 | describe 'app', -> 3 | mocks = require 'mocks' 4 | loadFile = mocks.loadFile 5 | app = done = null 6 | 7 | beforeEach -> 8 | done = jasmine.createSpy 'done' 9 | # load the file and inject fake fs module 10 | app = loadFile __dirname + '/app.js', 11 | fs: mocks.fs.create 12 | 'bin': 13 | 'run.sh': 1, 14 | 'install.sh': 1 15 | 'home': 16 | 'some.js': 1, 17 | 'another.txt': 1 18 | 'one.js': 1, 19 | 'two.js': 1, 20 | 'three.js': 1 21 | 22 | 23 | it 'should return only existing files', -> 24 | done.andCallFake (err, filtered) -> 25 | expect(err).toBeFalsy() 26 | expect(filtered).toEqual ['/bin/run.sh'] 27 | 28 | app.filterOnlyExistingFiles ['/bin/run.sh', '/non.txt'], done 29 | waitsFor -> done.callCount 30 | 31 | it 'should ignore directories', -> 32 | done.andCallFake (err, filtered) -> 33 | expect(filtered).toEqual ['/bin/run.sh', '/home/some.js'] 34 | 35 | app.filterOnlyExistingFiles ['/bin/run.sh', '/home', '/home/some.js'], done 36 | waitsFor -> done.callCount 37 | 38 | it 'should preserve order', -> 39 | done.andCallFake (err, filtered) -> 40 | expect(filtered).toEqual ['/one.js', '/two.js', '/three.js'] 41 | 42 | # mock fs will call back in this pattern order 43 | mocks.predictableNextTick.pattern = [2, 0, 1] 44 | app.filterOnlyExistingFiles ['/one.js', '/two.js', '/three.js'], done 45 | waitsFor -> done.callCount 46 | -------------------------------------------------------------------------------- /jsl.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration File for JavaScript Lint 0.3.0 3 | # Developed by Matthias Miller (http://www.JavaScriptLint.com) 4 | # 5 | # This configuration file can be used to lint a collection of scripts, or to enable 6 | # or disable warnings for scripts that are linted via the command line. 7 | # 8 | 9 | ### Warnings 10 | # Enable or disable warnings based on requirements. 11 | # Use "+WarningName" to display or "-WarningName" to suppress. 12 | # 13 | -no_return_value # function {0} does not always return a value 14 | +duplicate_formal # duplicate formal argument {0} 15 | -equal_as_assign # test for equality (==) mistyped as assignment (=)?{0} 16 | +var_hides_arg # variable {0} hides argument 17 | +redeclared_var # redeclaration of {0} {1} 18 | -anon_no_return_value # anonymous function does not always return a value 19 | +missing_semicolon # missing semicolon 20 | +meaningless_block # meaningless block; curly braces have no impact 21 | +comma_separated_stmts # multiple statements separated by commas (use semicolons?) 22 | -unreachable_code # unreachable code 23 | -missing_break # missing break statement 24 | +missing_break_for_last_case # missing break statement for last case in switch 25 | +comparison_type_conv # comparisons against null, 0, true, false, or an empty string allowing implicit type conversion (use === or !==) 26 | -inc_dec_within_stmt # increment (++) and decrement (--) operators used as part of greater statement 27 | +useless_void # use of the void type may be unnecessary (void is always undefined) 28 | +multiple_plus_minus # unknown order of operations for successive plus (e.g. x+++y) or minus (e.g. x---y) signs 29 | +use_of_label # use of label 30 | -block_without_braces # block statement without curly braces 31 | +leading_decimal_point # leading decimal point may indicate a number or an object member 32 | +trailing_decimal_point # trailing decimal point may indicate a number or an object member 33 | +octal_number # leading zeros make an octal number 34 | +nested_comment # nested comment 35 | -misplaced_regex # regular expressions should be preceded by a left parenthesis, assignment, colon, or comma 36 | +ambiguous_newline # unexpected end of line; it is ambiguous whether these lines are part of the same statement 37 | +empty_statement # empty statement or extra semicolon 38 | -missing_option_explicit # the "option explicit" control comment is missing 39 | +partial_option_explicit # the "option explicit" control comment, if used, must be in the first script tag 40 | +dup_option_explicit # duplicate "option explicit" control comment 41 | +useless_assign # useless assignment 42 | +ambiguous_nested_stmt # block statements containing block statements should use curly braces to resolve ambiguity 43 | +ambiguous_else_stmt # the else statement could be matched with one of multiple if statements (use curly braces to indicate intent) 44 | -missing_default_case # missing default case in switch statement 45 | +duplicate_case_in_switch # duplicate case in switch statements 46 | +default_not_at_end # the default case is not at the end of the switch statement 47 | +legacy_cc_not_understood # couldn't understand control comment using /*@keyword@*/ syntax 48 | +jsl_cc_not_understood # couldn't understand control comment using /*jsl:keyword*/ syntax 49 | +useless_comparison # useless comparison; comparing identical expressions 50 | +with_statement # with statement hides undeclared variables; use temporary variable instead 51 | +trailing_comma_in_array # extra comma is not recommended in array initializers 52 | +assign_to_function_call # assignment to a function call 53 | +parseint_missing_radix # parseInt missing radix parameter 54 | 55 | 56 | ### Output format 57 | # Customize the format of the error message. 58 | # __FILE__ indicates current file path 59 | # __FILENAME__ indicates current file name 60 | # __LINE__ indicates current line 61 | # __ERROR__ indicates error message 62 | # 63 | # Visual Studio syntax (default): 64 | +output-format __FILE__(__LINE__): __ERROR__ 65 | # Alternative syntax: 66 | #+output-format __FILE__:__LINE__: __ERROR__ 67 | 68 | 69 | ### Context 70 | # Show the in-line position of the error. 71 | # Use "+context" to display or "-context" to suppress. 72 | # 73 | +context 74 | 75 | 76 | ### Semicolons 77 | # By default, assignments of an anonymous function to a variable or 78 | # property (such as a function prototype) must be followed by a semicolon. 79 | # 80 | +lambda_assign_requires_semicolon 81 | 82 | 83 | ### Control Comments 84 | # Both JavaScript Lint and the JScript interpreter confuse each other with the syntax for 85 | # the /*@keyword@*/ control comments and JScript conditional comments. (The latter is 86 | # enabled in JScript with @cc_on@). The /*jsl:keyword*/ syntax is preferred for this reason, 87 | # although legacy control comments are enabled by default for backward compatibility. 88 | # 89 | +legacy_control_comments 90 | 91 | 92 | ### JScript Function Extensions 93 | # JScript allows member functions to be defined like this: 94 | # function MyObj() { /*constructor*/ } 95 | # function MyObj.prototype.go() { /*member function*/ } 96 | # 97 | # It also allows events to be attached like this: 98 | # function window::onload() { /*init page*/ } 99 | # 100 | # This is a Microsoft-only JavaScript extension. Enable this setting to allow them. 101 | # 102 | -jscript_function_extensions 103 | 104 | 105 | ### Defining identifiers 106 | # By default, "option explicit" is enabled on a per-file basis. 107 | # To enable this for all files, use "+always_use_option_explicit" 108 | -always_use_option_explicit 109 | 110 | # Define certain identifiers of which the lint is not aware. 111 | # (Use this in conjunction with the "undeclared identifier" warning.) 112 | # 113 | # Common uses for webpages might be: 114 | #+define window 115 | #+define document 116 | 117 | 118 | ### Files 119 | # Specify which files to lint 120 | # Use "+recurse" to enable recursion (disabled by default). 121 | # To add a set of files, use "+process FileName", "+process Folder\Path\*.js", 122 | # or "+process Folder\Path\*.htm". 123 | # 124 | +process lib/*.js 125 | -------------------------------------------------------------------------------- /lib/chokidar.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | 3 | var FSWatcher = function() { 4 | // mock API 5 | this.watchedPaths_ = []; 6 | 7 | this.add = function(path) { 8 | this.watchedPaths_.push(path); 9 | }; 10 | }; 11 | 12 | FSWatcher.prototype = new events.EventEmitter(); 13 | 14 | 15 | // PUBLIC 16 | exports.FSWatcher = FSWatcher; 17 | -------------------------------------------------------------------------------- /lib/fs.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var path = require('path'); 3 | var predictableNextTick = require('./util.js').predictableNextTick; 4 | 5 | 6 | /** 7 | * @constructor 8 | * @param {boolean} isDirectory 9 | */ 10 | var Stats = function(isFile, mtime) { 11 | this.mtime = mtime; 12 | 13 | this.isDirectory = function() { 14 | return !isFile; 15 | }; 16 | 17 | this.isFile = function() { 18 | return isFile; 19 | }; 20 | }; 21 | 22 | 23 | var File = function(mtime, content) { 24 | this.mtime = mtime; 25 | this.content = content || ''; 26 | 27 | this.getStats = function() { 28 | return new Stats(true, new Date(this.mtime)); 29 | }; 30 | 31 | this.getBuffer = function() { 32 | return new Buffer(this.content); 33 | }; 34 | }; 35 | 36 | 37 | var Directory = function(mtime) { 38 | this.mtime = mtime; 39 | 40 | this.getStats = function() { 41 | return new Stats(false, new Date(this.mtime)); 42 | }; 43 | }; 44 | 45 | 46 | /** 47 | * @constructor 48 | * @param {Object} structure 49 | */ 50 | var Mock = function(structure) { 51 | var watchers = {}; 52 | var cwd = '/'; 53 | 54 | // TODO(vojta): convert structure to contain only instances of File/Directory (not primitives) 55 | 56 | var getPointer = function(filepath, pointer) { 57 | var absPath = path.resolve(cwd, filepath); 58 | var parts = absPath.replace(/\\/g, '/').replace(/\/$/, '').split('/').slice(1); 59 | 60 | while (parts.length) { 61 | if (!pointer[parts[0]]) break; 62 | pointer = pointer[parts.shift()]; 63 | } 64 | 65 | return parts.length ? null : pointer; 66 | }; 67 | 68 | 69 | var validatePath = function(path) { 70 | if (path.charAt(0) !== '/') { 71 | throw new Error('Relative path not supported !'); 72 | } 73 | }; 74 | 75 | 76 | // public API 77 | this.stat = function(path, callback) { 78 | var statSync = this.statSync; 79 | 80 | predictableNextTick(function() { 81 | var stat = null; 82 | var error = null; 83 | 84 | try { 85 | stat = statSync(path); 86 | } catch(e) { 87 | error = e; 88 | } 89 | 90 | callback(error, stat); 91 | }); 92 | }; 93 | 94 | 95 | this.statSync = function(path) { 96 | validatePath(path); 97 | 98 | var pointer = getPointer(path, structure); 99 | 100 | if (!pointer) { 101 | var error = new Error('ENOENT, no such file or directory "' + path + '"'); 102 | 103 | error.errno = 34; 104 | error.code = 'ENOENT'; 105 | 106 | throw error; 107 | } 108 | 109 | return pointer instanceof File ? pointer.getStats() : new Stats(typeof pointer !== 'object'); 110 | }; 111 | 112 | 113 | this.readdir = function(path, callback) { 114 | validatePath(path); 115 | predictableNextTick(function() { 116 | var pointer = getPointer(path, structure); 117 | return pointer && typeof pointer === 'object' && !(pointer instanceof File) ? 118 | callback(null, Object.getOwnPropertyNames(pointer).sort()) : callback({}); 119 | }); 120 | }; 121 | 122 | 123 | this.readdirSync = function(path, callback) { 124 | validatePath(path); 125 | 126 | var pointer = getPointer(path, structure); 127 | var error; 128 | 129 | if (!pointer) { 130 | error = new Error('ENOENT, no such file or directory "' + path + '"'); 131 | error.errno = 34; 132 | error.code = 'ENOENT'; 133 | 134 | throw error; 135 | } 136 | 137 | if(pointer instanceof File) { 138 | error = new Error('ENOTDIR, not a directory "' + path + '"'); 139 | error.errno = 27; 140 | error.code = 'ENOTDIR'; 141 | 142 | throw error; 143 | } 144 | 145 | return Object.keys(pointer); 146 | }; 147 | 148 | 149 | this.mkdir = function(directory, callback) { 150 | var mkdirSync = this.mkdirSync; 151 | var error = null; 152 | 153 | predictableNextTick(function() { 154 | try { 155 | mkdirSync(directory); 156 | } catch (e) { 157 | error = e; 158 | } 159 | 160 | callback(error); 161 | }); 162 | }; 163 | 164 | 165 | this.mkdirSync = function(directory) { 166 | var pointer = getPointer(path.dirname(directory), structure); 167 | var baseName = path.basename(directory); 168 | 169 | if (pointer && typeof pointer === 'object' && !(pointer instanceof File)) { 170 | pointer[baseName] = {}; 171 | return; 172 | } 173 | 174 | var error = new Error(util.format('ENOENT, mkdir "%s"', directory)); 175 | error.code = 'ENOENT'; 176 | error.errno = 34; 177 | 178 | throw error; 179 | }; 180 | 181 | 182 | this.readFile = function(path, encoding, callback) { 183 | var readFileSync = this.readFileSync; 184 | callback = callback || encoding; 185 | 186 | predictableNextTick(function() { 187 | var data = null; 188 | var error = null; 189 | 190 | try { 191 | data = readFileSync(path); 192 | } catch(e) { 193 | error = e; 194 | } 195 | 196 | callback(error, data); 197 | }); 198 | }; 199 | 200 | 201 | this.readFileSync = function(path) { 202 | var pointer = getPointer(path, structure); 203 | var error; 204 | 205 | if (!pointer) { 206 | error = new Error(util.format('No such file or directory "%s"', path)); 207 | error.code = 'ENOENT'; 208 | 209 | throw error; 210 | } 211 | 212 | if (pointer instanceof File) { 213 | return pointer.getBuffer(); 214 | } 215 | 216 | if (typeof pointer === 'object') { 217 | error = new Error('Illegal operation on directory'); 218 | error.code = 'EISDIR'; 219 | 220 | throw error; 221 | } 222 | 223 | return new Buffer(''); 224 | }; 225 | 226 | 227 | this.writeFile = function(filePath, content, callback) { 228 | predictableNextTick(function() { 229 | var pointer = getPointer(path.dirname(filePath), structure); 230 | var baseName = path.basename(filePath); 231 | 232 | if (pointer && typeof pointer === 'object' && !(pointer instanceof File)) { 233 | pointer[baseName] = new File(0, content); 234 | callback(null); 235 | } else { 236 | var error = new Error(util.format('Can not open "%s"', filePath)); 237 | error.code = 'ENOENT'; 238 | callback(error); 239 | } 240 | }); 241 | }; 242 | 243 | 244 | this.watchFile = function(path, options, callback) { 245 | callback = callback || options; 246 | watchers[path] = watchers[path] || []; 247 | watchers[path].push(callback); 248 | }; 249 | 250 | 251 | this.exists = function(path, callback) { 252 | var existsSync = this.existsSync; 253 | 254 | predictableNextTick(function() { 255 | callback(existsSync(path)); 256 | }); 257 | }; 258 | 259 | 260 | this.existsSync = function(path) { 261 | return getPointer(path, structure) != null; 262 | } 263 | 264 | 265 | // Mock API 266 | this._touchFile = function(path, mtime, content) { 267 | var pointer = getPointer(path, structure); 268 | var previous = pointer.getStats(); 269 | 270 | // update the file 271 | if (typeof mtime !== 'undefined') pointer.mtime = mtime; 272 | if (typeof content !== 'undefined') pointer.content = content; 273 | 274 | var current = pointer.getStats(); 275 | (watchers[path] || []).forEach(function(callback) { 276 | callback(current, previous); 277 | }); 278 | }; 279 | 280 | this._setCWD = function(path) { 281 | cwd = path; 282 | }; 283 | }; 284 | 285 | 286 | // PUBLIC stuff 287 | exports.create = function(structure) { 288 | return new Mock(structure); 289 | }; 290 | 291 | exports.file = function(mtime, content) { 292 | return new File(mtime, content); 293 | }; 294 | -------------------------------------------------------------------------------- /lib/glob.js: -------------------------------------------------------------------------------- 1 | var predictableNextTick = require('./util').predictableNextTick; 2 | 3 | 4 | // Trivial mock for https://github.com/isaacs/node-glob 5 | // It uses predictableNextTick, so that you can simulate different order of callbacks 6 | // 7 | // Creates new instance of glob 8 | exports.create = function(results) { 9 | return function(pattern, options, done) { 10 | var result = results[pattern]; 11 | 12 | if (!result) { 13 | throw new Error('Unexpected glob for "' + pattern + '"!'); 14 | } 15 | 16 | predictableNextTick(function() { 17 | done(null, result); 18 | }); 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /lib/http.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var EventEmitter = require('events').EventEmitter; 3 | 4 | 5 | var ServerResponse = function() { 6 | var bodySent = false; 7 | 8 | this._headers = {}; 9 | this._body = null; 10 | this._status = null; 11 | 12 | this._isFinished = function() { 13 | return this.headerSent && bodySent; 14 | }; 15 | 16 | this.headerSent = false; 17 | 18 | this.setHeader = function(name, value) { 19 | if (this.headerSent) { 20 | throw new Error("Can't set headers after they are sent."); 21 | } 22 | 23 | this._headers[name] = value; 24 | }; 25 | 26 | this.getHeader = function(name) { 27 | return this._headers[name]; 28 | }; 29 | 30 | this.removeHeader = function(name) { 31 | delete this._headers[name]; 32 | }; 33 | 34 | this.writeHead = function(status) { 35 | if (this.headerSent) { 36 | throw new Error("Can't render headers after they are sent to the client."); 37 | } 38 | 39 | this.headerSent = true; 40 | this._status = status; 41 | }; 42 | 43 | this.write = function(content) { 44 | if (bodySent) { 45 | throw new Error("Can't write to already finished response."); 46 | } 47 | 48 | this._body = this._body ? this._body + content.toString() : content.toString(); 49 | }; 50 | 51 | this.end = function(content) { 52 | if (content) { 53 | this.write(content ); 54 | } 55 | 56 | bodySent = true; 57 | this.emit('end'); 58 | }; 59 | }; 60 | 61 | util.inherits(ServerResponse, EventEmitter); 62 | 63 | var ServerRequest = function(url, headers) { 64 | this.url = url; 65 | this.headers = headers || {}; 66 | 67 | this.getHeader = function(key) { 68 | return this.headers[key]; 69 | }; 70 | }; 71 | 72 | util.inherits(ServerRequest, EventEmitter); 73 | 74 | 75 | // PUBLIC stuff 76 | exports.ServerResponse = ServerResponse; 77 | exports.ServerRequest = ServerRequest; 78 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var util = require('./util'); 2 | 3 | exports.fs = require('./fs'); 4 | exports.http = require('./http'); 5 | exports.predictableNextTick = util.predictableNextTick; 6 | exports.loadFile = util.loadFile; 7 | exports.glob = require('./glob'); 8 | exports.chokidar = require('./chokidar'); 9 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var vm = require('vm'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | var nextTickQueue = []; 6 | var nextTickRegistered = false; 7 | var pointerBase = 0; 8 | var pointerInPattern = 0; 9 | 10 | 11 | var reset = function() { 12 | nextTickRegistered = false; 13 | nextTickQueue.length = 0; 14 | pointerBase = 0; 15 | pointerInPattern = 0; 16 | predictableNextTick.pattern = [0]; 17 | }; 18 | 19 | 20 | var nextTickHandler = function() { 21 | var pattern = predictableNextTick.pattern; 22 | 23 | while (pointerBase < nextTickQueue.length) { 24 | while (pointerInPattern < pattern.length) { 25 | var index = pointerBase + pattern[pointerInPattern]; 26 | 27 | if (nextTickQueue[index]) { 28 | try { 29 | nextTickQueue[index](); 30 | nextTickQueue[index] = null; 31 | } catch(e) { 32 | nextTickQueue[index] = null; 33 | 34 | // just in the case someone will handle the exception 35 | if (nextTickQueue.some(function(fn) { return fn; })) { 36 | process.nextTick(nextTickHandler); 37 | } else { 38 | reset(); 39 | } 40 | 41 | throw e; 42 | } 43 | } else { 44 | // fill skipped holes, so that predictableNextTick() won't push into these holes 45 | while (nextTickQueue.length < index + 1) { 46 | nextTickQueue.push(null); 47 | } 48 | } 49 | pointerInPattern++; 50 | } 51 | pointerInPattern = 0; 52 | pointerBase += pattern.length; 53 | } 54 | 55 | reset(); 56 | }; 57 | 58 | var predictableNextTick = function(callback) { 59 | nextTickQueue.push(callback); 60 | 61 | if (!nextTickRegistered) { 62 | process.nextTick(nextTickHandler); 63 | nextTickRegistered = true; 64 | } 65 | }; 66 | 67 | predictableNextTick.pattern = [0]; 68 | 69 | /** 70 | * Helper for unit testing: 71 | * - load module with mocked dependencies 72 | * - allow accessing private state of the module 73 | * 74 | * @param {string} path Absolute path to module (file to load) 75 | * @param {Object=} mocks Hash of mocked dependencies 76 | * @param {Object=} globals Hash of globals () 77 | * @param {boolean} mockNested If true, mock even nested requires. Default to false 78 | */ 79 | var loadFile = function(filePath, mocks, globals, mockNested) { 80 | mocks = mocks || {}; 81 | globals = globals || {}; 82 | 83 | filePath = path.normalize(filePath); 84 | 85 | if (filePath.substr(-3) !== '.js') { 86 | filePath += '.js'; 87 | } 88 | 89 | var exports = {}; 90 | var context = { 91 | require: function(name) { 92 | // TODO(vojta): solve loading "localy installed" modules 93 | if (mocks.hasOwnProperty(name)) { 94 | return mocks[name]; 95 | } 96 | 97 | // this is necessary to allow relative path modules within loaded file 98 | // i.e. requiring ./some inside file /a/b.js needs to be resolved to /a/some 99 | if (name.charAt(0) !== '.') { 100 | return require(name); 101 | } 102 | 103 | var absolutePath = path.resolve(path.dirname(filePath), name); 104 | 105 | if (mockNested) { 106 | return loadFile(absolutePath, mocks, globals, mockNested).module.exports; 107 | } 108 | 109 | return require(absolutePath); 110 | }, 111 | __dirname: path.dirname(filePath), 112 | __filename: filePath, 113 | Buffer: Buffer, 114 | setTimeout: setTimeout, 115 | setInterval: setInterval, 116 | clearTimeout: clearTimeout, 117 | clearInterval: clearInterval, 118 | process: process, 119 | console: console, 120 | exports: exports, 121 | module: { 122 | exports: exports 123 | } 124 | }; 125 | 126 | Object.getOwnPropertyNames(globals || {}).forEach(function(name) { 127 | context[name] = globals[name]; 128 | }); 129 | 130 | vm.runInNewContext(fs.readFileSync(filePath), context, filePath); 131 | 132 | return context; 133 | }; 134 | 135 | 136 | // PUBLIC stuff 137 | exports.loadFile = loadFile; 138 | exports.predictableNextTick = predictableNextTick; 139 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mocks", 3 | "description": "Set of mock modules for easy testing (fs, http)", 4 | "homepage": "", 5 | "keywords": [ 6 | "mock", 7 | "stub", 8 | "dummy", 9 | "test double", 10 | "fake", 11 | "nodejs", 12 | "js", 13 | "testing", 14 | "test", 15 | "fs", 16 | "fs mock", 17 | "http", 18 | "http mock" 19 | ], 20 | "author": "Vojta Jína ", 21 | "contributors": [ 22 | "Damien Duportal ", 23 | "Taichi " 24 | ], 25 | "dependencies": {}, 26 | "devDependencies": { 27 | "coffee-script": "1.1.2", 28 | "jasmine-node": "1.0.11", 29 | "jake": "0.2.15" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git://github.com/vojtajina/node-mocks.git" 34 | }, 35 | "main": "./lib/index", 36 | "bin": {}, 37 | "engines": { 38 | "node": ">= 0.6.5" 39 | }, 40 | "version": "0.0.15" 41 | } -------------------------------------------------------------------------------- /test/chokidar.spec.coffee: -------------------------------------------------------------------------------- 1 | #============================================================================== 2 | # chokidar.js module 3 | #============================================================================== 4 | describe 'chokidar', -> 5 | chokidar = require '../lib/chokidar' 6 | watcher = null 7 | 8 | beforeEach -> 9 | watcher = new chokidar.FSWatcher 10 | 11 | it 'should store watched paths', -> 12 | watcher.add 'first.js' 13 | watcher.add ['/some/a.js', '/other/file.js'] 14 | 15 | expect(watcher.watchedPaths_).toContain 'first.js', '/some/a.js', '/other/file.js' 16 | 17 | 18 | it 'should be event emitter', -> 19 | spy = jasmine.createSpy 'onAdd' 20 | watcher.on 'add', spy 21 | 22 | watcher.emit 'add' 23 | expect(spy).toHaveBeenCalled() 24 | -------------------------------------------------------------------------------- /test/fixtures/other.js: -------------------------------------------------------------------------------- 1 | // A fake module for testing util.loadFile() 2 | 3 | exports.id = 'LOCAL_MODULE'; 4 | exports.fs = require('fs'); 5 | -------------------------------------------------------------------------------- /test/fixtures/some.js: -------------------------------------------------------------------------------- 1 | // A fake module for testing util.loadFile() 2 | 3 | var privateNumber = 100; 4 | var privateFs = require('fs'); 5 | var privateLocalModule = require('./other'); 6 | var privateConsole = console; 7 | -------------------------------------------------------------------------------- /test/fs.spec.coffee: -------------------------------------------------------------------------------- 1 | #============================================================================== 2 | # test/mock/fs.js module 3 | #============================================================================== 4 | describe 'fs', -> 5 | fsMock = require '../lib/fs' 6 | fs = callback = finished = null 7 | 8 | waitForFinished = (count = 1, name = 'FS') -> 9 | waitsFor (-> finished == count), name, 100 10 | 11 | beforeEach -> 12 | finished = 0 13 | 14 | fs = fsMock.create 15 | bin: 16 | grep: 1 17 | chmod: 1 18 | home: 19 | vojta: 20 | sub: 21 | 'first.js': 1 22 | 'second.js': 1 23 | 'third.log': 1 24 | sub2: 25 | 'first.js': 1 26 | 'second.js': 1 27 | 'third.log': 1 28 | 'some.js': fsMock.file '2012-01-01', 'some' 29 | 'another.js': fsMock.file '2012-01-02', 'content' 30 | 31 | 32 | # =========================================================================== 33 | # fs.stat() 34 | # =========================================================================== 35 | describe 'stat', -> 36 | 37 | it 'should be async', -> 38 | callback = jasmine.createSpy 'done' 39 | fs.stat '/bin', callback 40 | expect(callback).not.toHaveBeenCalled() 41 | 42 | 43 | it 'should stat directory', -> 44 | fs.stat '/bin', (err, stat) -> 45 | expect(err).toBeFalsy() 46 | expect(stat.isDirectory()).toBe true 47 | finished++ 48 | waitForFinished() 49 | 50 | 51 | it 'should stat file', -> 52 | callback = (err, stat) -> 53 | expect(err).toBeFalsy() 54 | expect(stat.isDirectory()).toBe false 55 | finished++ 56 | 57 | fs.stat '/bin/grep', callback 58 | fs.stat '/home/vojta/some.js', callback 59 | waitForFinished 2 60 | 61 | 62 | it 'should return error when path does not exist', -> 63 | callback = (err, stat) -> 64 | expect(err).toBeTruthy() 65 | expect(stat).toBeFalsy() 66 | finished++ 67 | 68 | fs.stat '/notexist', callback 69 | fs.stat '/home/notexist', callback 70 | waitForFinished 2 71 | 72 | 73 | it 'should have modified timestamp', -> 74 | callback = (err, stat) -> 75 | expect(err).toBeFalsy() 76 | expect(stat.mtime instanceof Date).toBe true 77 | expect(stat.mtime).toEqual new Date '2012-01-01' 78 | finished++ 79 | 80 | fs.stat '/home/vojta/some.js', callback 81 | waitForFinished() 82 | 83 | 84 | # =========================================================================== 85 | # fs.statSync() 86 | # =========================================================================== 87 | describe 'statSync', -> 88 | 89 | it 'should stat directory', -> 90 | stat = fs.statSync '/bin' 91 | expect(stat.isDirectory()).toBe true 92 | 93 | 94 | it 'should stat file', -> 95 | stat = fs.statSync '/bin/grep' 96 | expect(stat.isDirectory()).toBe false 97 | 98 | stat = fs.statSync '/home/vojta/some.js' 99 | expect(stat.isDirectory()).toBe false 100 | 101 | 102 | it 'should throw an error when path does not exist', -> 103 | expect(-> fs.statSync '/notexist').toThrow 'ENOENT, no such file or directory "/notexist"' 104 | 105 | 106 | # =========================================================================== 107 | # fs.readdir() 108 | # =========================================================================== 109 | describe 'readdir', -> 110 | 111 | it 'should be async', -> 112 | callback = jasmine.createSpy 'done' 113 | fs.readdir '/bin', callback 114 | expect(callback).not.toHaveBeenCalled() 115 | 116 | 117 | it 'should return array of files and directories', -> 118 | callback = (err, files) -> 119 | expect(err).toBeFalsy() 120 | expect(files).toContain 'sub' 121 | expect(files).toContain 'some.js' 122 | expect(files).toContain 'another.js' 123 | finished++ 124 | 125 | fs.readdir '/home/vojta', callback 126 | waitForFinished() 127 | 128 | 129 | it 'should return error if does not exist', -> 130 | callback = (err, files) -> 131 | expect(err).toBeTruthy() 132 | expect(files).toBeFalsy() 133 | finished++ 134 | 135 | fs.readdir '/home/not', callback 136 | waitForFinished() 137 | 138 | 139 | # =========================================================================== 140 | # fs.readdirSync 141 | # =========================================================================== 142 | describe 'readdirSync', -> 143 | 144 | it 'should read dir content and sync return all content files', -> 145 | content = fs.readdirSync '/home/vojta/sub' 146 | expect(content instanceof Array).toBe true 147 | expect(content).toEqual ['first.js','second.js','third.log'] 148 | 149 | 150 | it 'should throw when dir does not exist', -> 151 | expect(-> fs.readdirSync '/non-existing'). 152 | toThrow 'ENOENT, no such file or directory "/non-existing"' 153 | 154 | 155 | it 'should throw when reading a file', -> 156 | expect(-> fs.readdirSync '/home/vojta/some.js'). 157 | toThrow 'ENOTDIR, not a directory "/home/vojta/some.js"' 158 | 159 | 160 | # =========================================================================== 161 | # fs.mkdir() 162 | # =========================================================================== 163 | describe 'mkdir', -> 164 | 165 | it 'should be async', -> 166 | callback = jasmine.createSpy 'done' 167 | fs.mkdir '/bin', callback 168 | expect(callback).not.toHaveBeenCalled() 169 | 170 | 171 | it 'should create directory', -> 172 | callback = (err) -> 173 | expect(err).toBeFalsy() 174 | stat = fs.statSync '/home/new' 175 | expect(stat).toBeDefined() 176 | expect(stat.isDirectory()).toBe true 177 | finished++ 178 | 179 | fs.mkdir '/home/new', callback 180 | waitForFinished() 181 | 182 | 183 | it 'should create a root directory', -> 184 | callback = (err) -> 185 | expect(err).toBeFalsy() 186 | stat = fs.statSync '/new-root' 187 | expect(stat).toBeDefined() 188 | expect(stat.isDirectory()).toBe true 189 | finished++ 190 | 191 | fs.mkdir '/new-root', callback 192 | waitForFinished() 193 | 194 | 195 | it 'should return error if parent does not exist', -> 196 | callback = (err) -> 197 | expect(err).toBeTruthy() 198 | expect(err.errno).toBe 34 199 | expect(err.code).toBe 'ENOENT' 200 | finished++ 201 | 202 | fs.mkdir '/new/non/existing', callback 203 | waitForFinished() 204 | 205 | 206 | # =========================================================================== 207 | # fs.mkdirSync() 208 | # =========================================================================== 209 | describe 'mkdirSync', -> 210 | 211 | it 'should create directory', -> 212 | fs.mkdirSync '/home/new' 213 | expect(fs.statSync('/home/new').isDirectory()).toBe true 214 | 215 | 216 | # =========================================================================== 217 | # fs.readFile 218 | # =========================================================================== 219 | describe 'readFile', -> 220 | 221 | it 'should read file content as Buffer', -> 222 | callback = (err, data) -> 223 | expect(err).toBeFalsy() 224 | expect(data instanceof Buffer).toBe true 225 | expect(data.toString()).toBe 'some' 226 | finished++ 227 | 228 | fs.readFile '/home/vojta/some.js', callback 229 | waitForFinished() 230 | 231 | 232 | it 'should be async', -> 233 | callback = jasmine.createSpy 'calback' 234 | fs.readFile '/home/vojta/some.js', callback 235 | expect(callback).not.toHaveBeenCalled() 236 | 237 | 238 | it 'should call error callback when non existing file or directory', -> 239 | callback = (err, data) -> 240 | expect(err).toBeTruthy() 241 | finished++ 242 | 243 | fs.readFile '/home/vojta', callback 244 | fs.readFile '/some/non-existing', callback 245 | waitForFinished 2 246 | 247 | 248 | # regression 249 | it 'should not silent exception from callback', -> 250 | fs.readFile '/home/vojta/some.js', (err) -> 251 | throw 'CALLBACK EXCEPTION' if not err 252 | 253 | uncaughtExceptionCallback = (err) -> 254 | process.removeListener 'uncaughtException', uncaughtExceptionCallback 255 | expect(err).toEqual 'CALLBACK EXCEPTION' 256 | finished++ 257 | 258 | process.on 'uncaughtException', uncaughtExceptionCallback 259 | waitForFinished 1, 'exception', 100 260 | 261 | 262 | it 'should allow optional second argument (encoding)', -> 263 | fs.readFile '/home/vojta/some.js', 'utf-8', (err) -> 264 | finished++ 265 | 266 | waitForFinished() 267 | 268 | 269 | # =========================================================================== 270 | # fs.readFileSync 271 | # =========================================================================== 272 | describe 'readFileSync', -> 273 | 274 | it 'should read file content and sync return buffer', -> 275 | buffer = fs.readFileSync '/home/vojta/another.js' 276 | expect(buffer instanceof Buffer).toBe true 277 | expect(buffer.toString()).toBe 'content' 278 | 279 | 280 | it 'should throw when file does not exist', -> 281 | expect(-> fs.readFileSync '/non-existing'). 282 | toThrow 'No such file or directory "/non-existing"' 283 | 284 | 285 | it 'should throw when reading a directory', -> 286 | expect(-> fs.readFileSync '/home/vojta'). 287 | toThrow 'Illegal operation on directory' 288 | 289 | 290 | # =========================================================================== 291 | # fs.writeFile 292 | # =========================================================================== 293 | describe 'writeFile', -> 294 | 295 | it 'should write file content as Buffer', -> 296 | callback = (err) -> 297 | expect(err).toBeFalsy() 298 | finished++ 299 | 300 | fs.writeFile '/home/vojta/some.js', 'something', callback 301 | waitForFinished() 302 | 303 | runs -> 304 | expect(fs.readFileSync('/home/vojta/some.js').toString()).toBe 'something' 305 | 306 | 307 | it 'should return ENOENT when writing to non-existing directory', -> 308 | callback = (err) -> 309 | expect(err).toBeTruthy() 310 | expect(err instanceof Error).toBe true 311 | expect(err.code).toBe 'ENOENT' 312 | finished++ 313 | 314 | fs.writeFile '/home/vojta/non/existing/some.js', 'something', callback 315 | waitForFinished() 316 | 317 | 318 | # =========================================================================== 319 | # fs.watchFile 320 | # =========================================================================== 321 | describe 'watchFile', -> 322 | 323 | it 'should call when when file accessed', -> 324 | callback = jasmine.createSpy('watcher').andCallFake (current, previous) -> 325 | expect(current.isFile()).toBe true 326 | expect(previous.isFile()).toBe true 327 | expect(current.mtime).toEqual previous.mtime 328 | 329 | fs.watchFile '/home/vojta/some.js', callback 330 | expect(callback).not.toHaveBeenCalled() 331 | 332 | fs._touchFile '/home/vojta/some.js' 333 | expect(callback).toHaveBeenCalled() 334 | 335 | 336 | it 'should call when file modified', -> 337 | original = new Date '2012-01-01' 338 | modified = new Date '2012-01-02' 339 | 340 | callback = jasmine.createSpy('watcher').andCallFake (current, previous) -> 341 | expect(previous.mtime).toEqual original 342 | expect(current.mtime).toEqual modified 343 | 344 | fs.watchFile '/home/vojta/some.js', callback 345 | expect(callback).not.toHaveBeenCalled() 346 | 347 | fs._touchFile '/home/vojta/some.js', '2012-01-02', 'new content' 348 | expect(callback).toHaveBeenCalled() 349 | 350 | 351 | it 'should allow optional second argument (options)', -> 352 | callback = jasmine.createSpy 'watcher' 353 | fs.watchFile '/home/vojta/some.js', {some: 'options'}, callback 354 | fs._touchFile '/home/vojta/some.js' 355 | 356 | expect(callback).toHaveBeenCalled() 357 | 358 | 359 | # =========================================================================== 360 | # fs.existsSync 361 | # =========================================================================== 362 | describe 'existsSync', -> 363 | 364 | it 'should return true for existing file and false for non-existing', -> 365 | expect(fs.existsSync '/home/vojta/some.js').toBe(true) 366 | expect(fs.existsSync '/home/vojta/non-existing.js').toBe(false) 367 | 368 | 369 | # =========================================================================== 370 | # fs.exists 371 | # =========================================================================== 372 | describe 'exists', -> 373 | 374 | it 'should callback with true for existing file', -> 375 | callback = (exists) -> 376 | expect(exists).toBe(true) 377 | finished++ 378 | 379 | fs.exists '/home/vojta/some.js', callback 380 | waitForFinished() 381 | 382 | 383 | it 'should callback with false for none-existing file', -> 384 | callback = (exists) -> 385 | expect(exists).toBe(false) 386 | finished++ 387 | 388 | fs.exists '/home/vojta/none-existing.js', callback 389 | waitForFinished() 390 | 391 | 392 | describe 'relative paths', -> 393 | 394 | it 'should read file with a relative path', -> 395 | callback = (err, data) -> 396 | expect(err).toBeFalsy() 397 | expect(data instanceof Buffer).toBe true 398 | expect(data.toString()).toBe 'some' 399 | finished++ 400 | 401 | fs._setCWD '/home/vojta' 402 | fs.readFile './some.js', callback 403 | waitForFinished() 404 | -------------------------------------------------------------------------------- /test/glob.spec.coffee: -------------------------------------------------------------------------------- 1 | #============================================================================== 2 | # glob.js module 3 | #============================================================================== 4 | describe 'glob', -> 5 | createMock = require('../lib/glob').create 6 | predictableNextTick = require('../lib/util').predictableNextTick 7 | globMock = spy = null 8 | 9 | beforeEach -> 10 | spy = jasmine.createSpy 'done' 11 | globMock = createMock 12 | '/some/*.js': ['/some/a.js', '/some/b.js'] 13 | '*.txt': ['my.txt', 'other.txt'] 14 | 15 | it 'should be async', -> 16 | globMock '*.txt', null, spy 17 | expect(spy).not.toHaveBeenCalled(); 18 | 19 | it 'should return predefined results', -> 20 | spy.andCallFake (err, results) -> 21 | expect(err).toBeFalsy() 22 | expect(results).toEqual ['/some/a.js', '/some/b.js'] 23 | 24 | globMock '/some/*.js', null, spy 25 | waitsFor (-> spy.callCount), 'done callback', 100 26 | 27 | 28 | it 'should use predictableNextTick', -> 29 | predictableNextTick.pattern = [1, 0] 30 | globMock '*.txt', null, spy 31 | globMock '/some/*.js', null, spy 32 | 33 | waitsFor (-> spy.callCount is 2), 'both callbacks', 100 34 | runs -> 35 | # expect reversed order (because of predictableTick.pattern) 36 | expect(spy.argsForCall[0][1]).toEqual ['/some/a.js', '/some/b.js'] 37 | expect(spy.argsForCall[1][1]).toEqual ['my.txt', 'other.txt'] 38 | 39 | 40 | it 'should throw if unexpected glob', -> 41 | expect(-> globMock 'unexpected', null, spy).toThrow 'Unexpected glob for "unexpected"!' 42 | 43 | -------------------------------------------------------------------------------- /test/http.spec.coffee: -------------------------------------------------------------------------------- 1 | #============================================================================== 2 | # test/mock/http.js module 3 | #============================================================================== 4 | describe 'http', -> 5 | httpMock = require '../lib/http' 6 | 7 | #============================================================================== 8 | # http.ServerResponse 9 | #============================================================================== 10 | describe 'ServerResponse', -> 11 | response = null 12 | 13 | beforeEach -> 14 | response = new httpMock.ServerResponse 15 | 16 | it 'should set body', -> 17 | response.end 'Some Body' 18 | expect(response._body).toBe 'Some Body' 19 | 20 | 21 | it 'should convert body buffer to string', -> 22 | response.end new Buffer 'string' 23 | expect(response._body).toBe 'string' 24 | 25 | 26 | it 'should set status', -> 27 | response.writeHead 201 28 | expect(response._status).toBe 201 29 | 30 | 31 | it 'should throw when trygin to end() already finished reponse', -> 32 | response.end 'First Body' 33 | 34 | expect(-> response.end 'Another Body') 35 | .toThrow "Can't write to already finished response." 36 | expect(response._body).toBe 'First Body' 37 | 38 | 39 | it 'should set and remove headers', -> 40 | response.setHeader 'Content-Type', 'text/javascript' 41 | response.setHeader 'Cache-Control', 'no-cache' 42 | response.setHeader 'Content-Type', 'text/plain' 43 | response.removeHeader 'Cache-Control' 44 | 45 | expect(response._headers).toEqual {'Content-Type': 'text/plain'} 46 | 47 | 48 | it 'should getHeader()', -> 49 | response.setHeader 'Content-Type', 'json' 50 | expect(response.getHeader 'Content-Type').toBe 'json' 51 | 52 | 53 | it 'should throw when trying to send headers twice', -> 54 | response.writeHead 200 55 | 56 | expect(-> response.writeHead 200) 57 | .toThrow "Can't render headers after they are sent to the client." 58 | 59 | 60 | it 'should throw when trying to set headers after sending', -> 61 | response.writeHead 200 62 | 63 | expect(-> response.setHeader 'Some', 'Value') 64 | .toThrow "Can't set headers after they are sent." 65 | 66 | 67 | it 'should write body', -> 68 | response.write 'a' 69 | response.end 'b' 70 | 71 | expect(response._body).toBe 'ab' 72 | 73 | 74 | it 'should throw when trying to write after end', -> 75 | response.write 'one' 76 | response.end 'two' 77 | 78 | expect(-> response.write 'more') 79 | .toThrow "Can't write to already finished response." 80 | 81 | 82 | it 'isFinished() should assert whether headers and body has been sent', -> 83 | expect(response._isFinished()).toBe false 84 | 85 | response.setHeader 'Some', 'Value' 86 | expect(response._isFinished()).toBe false 87 | 88 | response.writeHead 404 89 | expect(response._isFinished()).toBe false 90 | 91 | response.end 'Some body' 92 | expect(response._isFinished()).toBe true 93 | 94 | 95 | #============================================================================== 96 | # http.ServerRequest 97 | #============================================================================== 98 | describe 'ServerRequest', -> 99 | 100 | it 'should return headers', -> 101 | request = new httpMock.ServerRequest '/some', {'Content-Type': 'json'} 102 | expect(request.getHeader 'Content-Type').toBe 'json' 103 | -------------------------------------------------------------------------------- /test/util.spec.coffee: -------------------------------------------------------------------------------- 1 | #============================================================================== 2 | # test/mock/util.js module 3 | #============================================================================== 4 | describe 'mock-util', -> 5 | util = require '../lib/util' 6 | 7 | #============================================================================ 8 | # util.predictableNextTick() 9 | #============================================================================ 10 | describe 'predictableNextTick', -> 11 | nextTick = util.predictableNextTick 12 | 13 | it 'should be async', -> 14 | spy = jasmine.createSpy 'nextTick callback' 15 | nextTick spy 16 | 17 | expect(spy).not.toHaveBeenCalled() 18 | waitsFor (-> spy.callCount), 'nextTick', 100 19 | 20 | 21 | it 'should behave predictable based on given pattern', -> 22 | stressIt = -> 23 | log = '' 24 | runs -> 25 | nextTick.pattern = [1, 0] 26 | 27 | nextTick -> log += 1 28 | nextTick -> log += 2 29 | nextTick -> log += 3 30 | nextTick -> log += 4 31 | waitsFor (-> log.length is 4), 'all nextTicks', 100 32 | runs -> 33 | expect(log).toBe '2143' 34 | 35 | # execute this test five times 36 | stressIt() for i in [1..5] 37 | 38 | 39 | it 'should do 021 pattern k*n fns', -> 40 | nextTick.pattern = [0, 2, 1] 41 | log = '' 42 | nextTick -> log += 0 43 | nextTick -> log += 1 44 | nextTick -> log += 2 45 | nextTick -> log += 3 46 | nextTick -> log += 4 47 | nextTick -> log += 5 48 | waitsFor (-> log.length is 6), 'all nextTicks', 100 49 | runs -> 50 | expect(log).toBe '021354' 51 | 52 | 53 | it 'should do 3021 pattern with n+1 fns', -> 54 | nextTick.pattern = [3, 0, 2, 1] 55 | log = '' 56 | nextTick -> log += 0 57 | nextTick -> log += 1 58 | nextTick -> log += 2 59 | nextTick -> log += 3 60 | nextTick -> log += 4 61 | waitsFor (-> log.length is 5), 'all nextTicks', 100 62 | runs -> 63 | expect(log).toBe '30214' 64 | 65 | 66 | # regression 67 | it 'should survive exception inside callback and fire callbacks registered afterwards', -> 68 | exceptionHandled = false 69 | beforeExceptionSpy = jasmine.createSpy 'before exception' 70 | afterExceptionSpy = jasmine.createSpy 'after exception' 71 | 72 | nextTick beforeExceptionSpy 73 | nextTick -> throw 'CALLBACK EXCEPTION' 74 | nextTick afterExceptionSpy 75 | 76 | uncaughtExceptionHandler = (err) -> 77 | process.removeListener 'uncaughtException', uncaughtExceptionHandler 78 | exceptionHandled = true 79 | 80 | process.on 'uncaughtException', uncaughtExceptionHandler 81 | waitsFor (-> afterExceptionSpy.callCount), 'after exception callback', 100 82 | runs -> 83 | expect(beforeExceptionSpy.callCount).toBe 1 84 | expect(afterExceptionSpy.callCount).toBe 1 85 | expect(exceptionHandled).toBe true 86 | 87 | 88 | # regression 89 | it 'should not ignore fn that was added into already skipped space during execution', -> 90 | nextTick.pattern = [1, 0] 91 | anotherCallback = jasmine.createSpy 'another later added fn' 92 | callback = jasmine.createSpy 'later added fn' 93 | 94 | nextTick -> 95 | nextTick -> 96 | callback() 97 | nextTick anotherCallback 98 | 99 | waitsFor (-> callback.callCount), 'later added fn to be called', 100 100 | waitsFor (-> anotherCallback.callCount), 'another later added fn to be called', 100 101 | 102 | 103 | it 'should follow pattern even if callbacks are nested', -> 104 | nextTick.pattern = [0, 2, 3, 1] 105 | log = [] 106 | 107 | nextTick -> 108 | log.push '0' 109 | nextTick -> 110 | log.push '01' 111 | nextTick -> 112 | log.push '02' 113 | 114 | nextTick -> 115 | log.push '1' 116 | 117 | waitsFor (-> log.length is 4), 'all callbacks processed', 100 118 | runs -> 119 | expect(log).toEqual ['0', '01', '02', '1'] 120 | 121 | 122 | # regression 123 | it 'should recover after error', -> 124 | spy = jasmine.createSpy 'nextTick callback' 125 | 126 | exceptionHandled = false 127 | uncaughtExceptionHandler = (err) -> 128 | process.removeListener 'uncaughtException', uncaughtExceptionHandler 129 | exceptionHandled = true 130 | 131 | process.on 'uncaughtException', uncaughtExceptionHandler 132 | 133 | nextTick -> 134 | throw new Error 'SOME ERR' 135 | 136 | waitsFor (-> exceptionHandled), 'exception handled', 100 137 | 138 | # register another tick callback, after the handled exception 139 | runs -> 140 | nextTick spy 141 | waitsFor (-> spy.callCount), 'spy being called', 100 142 | 143 | 144 | #============================================================================ 145 | # util.loadFile() 146 | #============================================================================ 147 | describe 'loadFile', -> 148 | loadFile = util.loadFile 149 | fixturePath = __dirname + '/fixtures/some.js' 150 | 151 | it 'should load file with access to private state', -> 152 | module = loadFile fixturePath 153 | expect(module.privateNumber).toBe 100 154 | 155 | 156 | it 'should inject mocks', -> 157 | fsMock = {} 158 | module = loadFile fixturePath, {fs: fsMock} 159 | expect(module.privateFs).toBe fsMock 160 | 161 | 162 | it 'should load local modules', -> 163 | module = loadFile fixturePath 164 | expect(module.privateLocalModule).toBeDefined() 165 | expect(module.privateLocalModule.id).toBe 'LOCAL_MODULE' 166 | 167 | 168 | it 'should inject globals', -> 169 | fakeConsole = {} 170 | module = loadFile fixturePath, {}, {console: fakeConsole} 171 | expect(module.privateConsole).toBe fakeConsole 172 | 173 | 174 | it 'should inject mocks into nested modules', -> 175 | fsMock = {} 176 | 177 | # /fixtures/some.js requires /fixtures/other.js 178 | # /fixtures/other.js requires fs 179 | module = loadFile fixturePath, {fs: fsMock}, {}, true 180 | 181 | expect(module.privateLocalModule.fs).toBe fsMock 182 | 183 | --------------------------------------------------------------------------------