├── .gitmodules ├── AUTHORS ├── LICENSE ├── README.md ├── bin ├── asynct └── test-output.sh ├── docs ├── assets │ ├── favicon.ico │ ├── icon.png │ ├── icon.svg │ └── style.css ├── index.html ├── running-tests.html └── writing-tests.html ├── index.js ├── lib ├── async_testing.js ├── child.js ├── console-runner.js ├── running.js ├── testing.js ├── web-runner.js ├── web-runner │ ├── child-loader.js │ └── public │ │ ├── error.png │ │ ├── failure.png │ │ ├── images.svg │ │ ├── index.html │ │ ├── main.js │ │ ├── running.gif │ │ ├── style.css │ │ └── success.png └── wrap.js ├── package.json ├── test ├── flow │ ├── flow-raw.js │ └── flow-wrap.js ├── test-all_passing.js ├── test-async_assertions.js ├── test-child_message_interference.js ├── test-custom_assertions.js ├── test-error_async.js ├── test-error_outside_suite.js ├── test-error_sync.js ├── test-error_syntax.js ├── test-error_test_already_finished.js ├── test-error_test_already_finished_async.js ├── test-error_uncaught_exception_handler.js ├── test-interference.js ├── test-overview.js ├── test-parse_run_arguments.js ├── test-sub_suites.js ├── test-sync_assertions.js ├── test-uncaught_exception_handlers.js ├── test-weird_throws.js └── test-wrap_tests.js └── todo.txt /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/web-runner/public/client"] 2 | path = lib/web-runner/public/client 3 | url = http://github.com/LearnBoost/Socket.IO.git 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Authors ordered by first contribution. 2 | 3 | Benjamin Thomas 4 | Gabriel Farrell 5 | Clément Node 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Benjamin Thomas 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-async-testing 2 | ================== 3 | 4 | The node-async-testing project is no longer maintained. Perhaps you should take a 5 | look at [asynct](https://github.com/dominictarr/asynct) or [node-unit](https://github.com/caolan/nodeunit). 6 | 7 | The author of this project feels that testing frameworks like this are inherently 8 | flawed in an async environment. They depend on your tests working how you expect 9 | them to, but the whole point of a testing framework is that you catch things you 10 | don't expect. 11 | 12 | I do have some ideas for addressing this, but until that time, node-async-testing 13 | will have to wait... 14 | -------------------------------------------------------------------------------- /bin/asynct: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | try { 4 | // always check for a local copy of async_testing first 5 | var testing = require('../lib/async_testing'); 6 | } 7 | catch(err) { 8 | if( err.message == "Cannot find module '../lib/async_testing'" ) { 9 | // look in the path for async_testing 10 | var testing = require('async-testing'); 11 | } 12 | else { 13 | throw err; 14 | } 15 | } 16 | 17 | exports.test = function (test){ 18 | test.ok(false,"this should not be called!") 19 | } 20 | process.ARGV.shift() //node 21 | process.ARGV.shift() // this file... if i leave this in it tried to run this file as a test, which goes into infinite loop and doesn't exit. 22 | process.ARGV.unshift('node') 23 | 24 | testing.run(process.ARGV); 25 | 26 | -------------------------------------------------------------------------------- /bin/test-output.sh: -------------------------------------------------------------------------------- 1 | extras=$@ 2 | 3 | group() { 4 | echo $1 5 | echo "==============================================================================================" 6 | } 7 | run() { 8 | echo $ "$@" $extras 9 | "$@" $extras 10 | echo "----------------------------------------------------------------------------------------------" 11 | } 12 | 13 | echo "" 14 | 15 | group "mixed multiple" 16 | run node test/test-sync_assertions.js test/test-all_passing.js -0 17 | run node test/test-sync_assertions.js test/test-all_passing.js -1 18 | run node test/test-sync_assertions.js test/test-all_passing.js -2 19 | 20 | group "make sure log level flag works" 21 | run node test/test-sync_assertions.js test/test-all_passing.js --log-level 0 22 | 23 | group "make sure '--all' works with each output level" 24 | run node test/test-sync_assertions.js test/test-all_passing.js -0 --all 25 | run node test/test-sync_assertions.js test/test-all_passing.js -1 --all 26 | run node test/test-sync_assertions.js test/test-all_passing.js -2 --all 27 | 28 | group "make sure you can do no color" 29 | run node test/test-sync_assertions.js test/test-all_passing.js --no-color 30 | 31 | group "all passing, one suite" 32 | run node test/test-all_passing.js -0 33 | run node test/test-all_passing.js -1 34 | 35 | group "all passing, multiple suites" 36 | run node test/test-all_passing.js test/test-overview.js -0 37 | run node test/test-all_passing.js test/test-overview.js -1 38 | 39 | group "failures, one suite" 40 | run node test/test-sync_assertions.js -0 41 | run node test/test-sync_assertions.js -1 42 | 43 | group "failures, multiple suites" 44 | run node test/test-sync_assertions.js test/test-async_assertions.js -0 45 | run node test/test-sync_assertions.js test/test-async_assertions.js -1 46 | 47 | group "test errors, one suite" 48 | run node test/test-error_sync.js -0 49 | run node test/test-error_sync.js -1 50 | 51 | group "test errors, multiple suites" 52 | run node test/test-error_sync.js test/test-error_async.js -0 53 | run node test/test-error_sync.js test/test-error_async.js -1 54 | 55 | group "test multiple errors, in parallel" 56 | run node test/test-error_async.js -0 -p 57 | run node test/test-error_async.js -1 -p 58 | 59 | group "test load error, one suite" 60 | run node test/test-error_outside_suite.js -0 61 | run node test/test-error_outside_suite.js -1 62 | run node test/test-error_outside_suite.js -2 63 | 64 | group "test load error, multiple suites" 65 | run node test/test-error_outside_suite.js test/test-error_syntax.js -0 66 | run node test/test-error_outside_suite.js test/test-error_syntax.js -1 67 | run node test/test-error_outside_suite.js test/test-error_syntax.js -2 68 | 69 | group "--test-name" 70 | run node test/test-all_passing.js --all --test-name "test A" --test-name "test B" 71 | run node test/test-all_passing.js test/test-sync_assertions.js --all --test-name "test A" --test-name "test fail" 72 | 73 | group "--suite-name" 74 | run node test/test-all_passing.js test/test-sync_assertions.js test/test-async_assertions.js --suite-name "test/test-all_passing.js" --all 75 | run node test/test-all_passing.js test/test-sync_assertions.js test/test-async_assertions.js --suite-name "test/test-all_passing.js" --suite-name "test/test-sync_assertions.js" --all 76 | -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bentomas/node-async-testing/82384ba4b8444e4659464bad661788827ea721aa/docs/assets/favicon.ico -------------------------------------------------------------------------------- /docs/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bentomas/node-async-testing/82384ba4b8444e4659464bad661788827ea721aa/docs/assets/icon.png -------------------------------------------------------------------------------- /docs/assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 27 | 28 | 46 | 48 | 49 | 51 | image/svg+xml 52 | 54 | 55 | 56 | 57 | 58 | 63 | 70 | 78 | 91 | 99 | 112 | 120 | 128 | 136 | 144 | 152 | 160 | 168 | 176 | 184 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /docs/assets/style.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | color: #222; 4 | font-family: 'Helvetica', 'Arial', sans-serif; 5 | line-height: 1.4em; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | a { 11 | color: #0000FF; 12 | font-weight: bold; 13 | text-decoration: none; 14 | } 15 | a:hover { 16 | text-decoration: underline; 17 | } 18 | 19 | header, section, nav { 20 | display: block; 21 | } 22 | 23 | #container { 24 | margin: auto; 25 | max-width: 708px; 26 | } 27 | 28 | #main-header { 29 | background: url(icon.png) no-repeat 0 100%; 30 | padding: 50px 0 5px 90px; 31 | margin: 0 0 -5px 140px; 32 | } 33 | #main-header h1 { 34 | margin: 0; 35 | padding: 30px 0 0; 36 | } 37 | #main-header h2 { 38 | font-size: 1.2em; 39 | font-weight: 1.2em; 40 | margin: 0; 41 | padding: 5px 0 5px; 42 | text-transform: lowercase; 43 | } 44 | 45 | nav ul { 46 | list-style-type: none; 47 | margin: 25px 0 -25px; 48 | padding: 0; 49 | text-align: center; 50 | } 51 | nav li { 52 | display: inline; 53 | margin: 15px; 54 | } 55 | nav a { 56 | color: black; 57 | font-weight: bold; 58 | text-decoration: none; 59 | text-transform: lowercase; 60 | } 61 | nav a:hover { 62 | text-decoration: underline; 63 | } 64 | nav .current a { 65 | color: #0000FF; 66 | } 67 | 68 | section { 69 | margin: 50px 0; 70 | } 71 | section h1 { 72 | font-size: 20px; 73 | font-weight: normal; 74 | margin-left: -15px; 75 | } 76 | section h2 { 77 | font-size: 15px; 78 | } 79 | section h2 code { 80 | font-weight: normal; 81 | } 82 | #opinions h2 { 83 | margin-bottom: 0; 84 | } 85 | #opinions h2+p { 86 | margin-top: 0; 87 | } 88 | 89 | 90 | code { 91 | background: #F8F8FF; 92 | border: 1px solid #DEDEDE; 93 | color: #444; 94 | font-size: 1.1em; 95 | padding: 0 2px 2px; 96 | } 97 | h1 code { 98 | font-size: .9em; 99 | } 100 | 101 | video#demo { 102 | margin: 0 -50px; 103 | } 104 | 105 | .toc { 106 | list-style-type: disc; 107 | line-height: 1.6em; 108 | padding: 0 0 0 20px; 109 | margin-left: 0; 110 | } 111 | .toc ul { 112 | list-style-type: square; 113 | padding: 0 0 0 20px; 114 | margin-left: 0; 115 | } 116 | 117 | dd { 118 | margin: 5px 0 10px 20px; 119 | } 120 | 121 | .highlight { 122 | background: #F8F8FF; 123 | border: 1px solid #DEDEDE; 124 | padding: 5px 10px; 125 | } 126 | .highlight pre { margin: 0; } 127 | .highlight .hll { background-color: #ffffcc } 128 | .highlight .c { color: #60a0b0; font-style: italic } /* Comment */ 129 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 130 | .highlight .k { color: #007020; font-weight: bold } /* Keyword */ 131 | .highlight .o { color: #666666 } /* Operator */ 132 | .highlight .cm { color: #60a0b0; font-style: italic } /* Comment.Multiline */ 133 | .highlight .cp { color: #007020 } /* Comment.Preproc */ 134 | .highlight .c1 { color: #60a0b0; font-style: italic } /* Comment.Single */ 135 | .highlight .cs { color: #60a0b0; background-color: #fff0f0 } /* Comment.Special */ 136 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 137 | .highlight .ge { font-style: italic } /* Generic.Emph */ 138 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 139 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 140 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 141 | .highlight .go { color: #808080 } /* Generic.Output */ 142 | .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 143 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 144 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 145 | .highlight .gt { color: #0040D0 } /* Generic.Traceback */ 146 | .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 147 | .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 148 | .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 149 | .highlight .kp { color: #007020 } /* Keyword.Pseudo */ 150 | .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 151 | .highlight .kt { color: #902000 } /* Keyword.Type */ 152 | .highlight .m { color: #40a070 } /* Literal.Number */ 153 | .highlight .s { color: #4070a0 } /* Literal.String */ 154 | .highlight .na { color: #4070a0 } /* Name.Attribute */ 155 | .highlight .nb { color: #007020 } /* Name.Builtin */ 156 | .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 157 | .highlight .no { color: #60add5 } /* Name.Constant */ 158 | .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 159 | .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 160 | .highlight .ne { color: #007020 } /* Name.Exception */ 161 | .highlight .nf { color: #06287e } /* Name.Function */ 162 | .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ 163 | .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 164 | .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ 165 | .highlight .nv { color: #bb60d5 } /* Name.Variable */ 166 | .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ 167 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 168 | .highlight .mf { color: #40a070 } /* Literal.Number.Float */ 169 | .highlight .mh { color: #40a070 } /* Literal.Number.Hex */ 170 | .highlight .mi { color: #40a070 } /* Literal.Number.Integer */ 171 | .highlight .mo { color: #40a070 } /* Literal.Number.Oct */ 172 | .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ 173 | .highlight .sc { color: #4070a0 } /* Literal.String.Char */ 174 | .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 175 | .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ 176 | .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 177 | .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ 178 | .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 179 | .highlight .sx { color: #c65d09 } /* Literal.String.Other */ 180 | .highlight .sr { color: #235388 } /* Literal.String.Regex */ 181 | .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ 182 | .highlight .ss { color: #517918 } /* Literal.String.Symbol */ 183 | .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 184 | .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ 185 | .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ 186 | .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ 187 | .highlight .il { color: #40a070 } /* Literal.Number.Integer.Long */ 188 | 189 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | node-async-testing — simple intuitive testing for node.js 6 | 7 | 8 | 9 | 10 |
11 |
12 |

node-async-testing

13 |

Simple, intuitive testing for node.js

14 |
15 | 16 | 23 | 24 |
25 |

Overview:

26 |

27 | node-async-testing is a fast, extendable uniting testing module for 28 | Node.js. It... 29 |

30 | 31 |
    32 |
  • fully embraces Node's async environment
  • 33 |
  • supports parallel test and suite execution
  • 34 |
  • has true test and suite setup and teardown functions
  • 35 |
  • 36 | helps your organize your suites by allowing you to group different 37 | tests together in sub-suites 38 |
  • 39 |
  • allows you to easily add your own custom assertion methods
  • 40 |
  • let's you customize test output for your particular needs
  • 41 |
42 | 43 |

Page outline:

44 | 45 | 51 |
52 | 53 |
54 |

node-async-testing's biases opinions:

55 | 56 |

Node is asynchronous, so testing should be too

57 |

58 | This applies to everything: assertions, errors, callbacks, setup, teardown, 59 | reports, and so on. 60 |

61 | 62 |

You should be able to run tests in parallel or serially

63 |

64 | Running tests in parallel is much faster, but makes it impossible to 65 | accurately deal with errors. 66 |

67 | 68 |

You should be able to test any code, and all aspects of it

69 |

Make no assumptions about the code being tested.

70 | 71 |

You shouldn't have to learn new assertion functions

72 |

73 | The assertion module that comes with Node is great. If you are familiar with 74 | it you won't have any problems, so no need to learn new functions (though 75 | you can add your own if you want). 76 |

77 | 78 |

No specifications, and no natural language suites

79 |

80 | Not another Behavior Driven Development testing framework. I don't like 81 | specifications and what not. They only add verbosity. "Text X" followed 82 | by a function is just right. 83 |

84 | 85 |

Test files should be executable by Node

86 |

87 | No preprocessors or custom scripts. If your test file is called 88 | my_test_file.js then node my_test_file.js should run it. 89 |

90 |
91 | 92 |
93 |

Installing:

94 | 95 |

With npm:

96 | 97 |
npm install async_testing
98 | 99 |

By hand:

100 |
101 | mkdir -p ~/.node_libraries
102 | cd ~/.node_libraries
103 | git clone --recursive git://github.com/bentomas/node-async-testing.git async_testing
104 |
105 | 106 |
107 |

Getting the source, Getting involved and Getting in touch:

108 | 109 |

110 | The source code for node-async-testing is 111 | located on GitHub. 112 | Feel free to send me pull requests, or 113 | report any issues you find. 114 | If you are looking for something to do, I maintain 115 | a list of things that 116 | need to be done or things I am thinking about doing. 117 |

118 | 119 |

120 | To get in contact with me directly, you can send me a message on GitHub or 121 | send me an email. Sometimes I can be 122 | an awful correspondent, so don't take it personally. My best friends get the same treatment. 123 | I will eventually get back to you, just sometimes it can take months. A little 124 | prodding every now and then can't hurt. 125 |

126 |
127 | 128 |
129 |

Where to start:

130 | 131 |

132 | Read about writing tests and suites or 133 | how to run your suites. 134 |

135 | 136 |

137 | Check out the examples. 138 | I recommend looking at test/test-overview.js first. 139 |

140 | 141 |

Watch this brief screencast:

142 | 143 | 146 |
147 | 148 |
149 | 150 | 151 | -------------------------------------------------------------------------------- /docs/running-tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | node-async-testing — simple intuitive testing for node.js 6 | 7 | 8 | 9 | 10 |
11 |
12 |

node-async-testing

13 |

Simple, intuitive testing for node.js

14 |
15 | 16 | 23 | 24 |
25 |

Running Test Suites

26 | 27 |

28 | node-async-testing includes two different means of running suites: 29 | A simple, recommended interface for running suite files and a low-level API 30 | (which is used by the previous method) which gives you fine-grained control. 31 |

32 | 33 |

Page outline:

34 | 35 | 40 |
41 | 42 |
43 |

Running Suite Files

44 | 45 |

46 | node-async-testing assumes you are going to have a one to one mapping 47 | between suites and files. So, to run a test suite, you actually tell it to 48 | run a file: 49 |

50 | 51 |
require('async_testing').run('test-suite.js');
52 | 53 |

54 | The run function can take a file name or a directory name (it 55 | recursively searches directories for javascript files that start with test-) 56 | or an array of any combination of those two options. 57 |

58 | 59 |

60 | In order for node-async-testing to run a file, the exports object of the 61 | file needs to be the test suite: 62 |

63 | 64 |
 65 | exports['first test'] = function(test) { ... };
 66 | exports['second test'] = function(test) { ... };
 67 | exports['third test'] = function(test) { ... };
 68 | 
 69 | // or
 70 | 
 71 | module.exports = {
 72 |   'first test': function(test) { ... },
 73 |   'second test': function(test) { ... },
 74 |   'third test': function(test) { ... }
 75 | };
76 | 77 |

78 | To make a file easy to execute with the node command, we need 79 | to make our file run the suite when it is the script being ran. Some where 80 | in the file we put this code (I like to put it at the very top): 81 |

82 | 83 |
 84 | // if this module is the script being run, then run the tests:
 85 | if (module === require.main) {
 86 |   return require('async_testing').run(__filename);
 87 | }
88 | 89 |

90 | Assuming we put that suite in a file called test-suite.js, we can now 91 | execute the it by running the following on the command line: 92 |

93 | 94 |
node test-suite.js
95 | 96 |

97 | The run function can also be passed the process.ARGV 98 | array of command line arguments, so node-async-testing settings can 99 | be altered at run time: 100 |

101 | 102 |
103 | if (module === require.main) {
104 |   return require('async_testing').run(process.ARGV);
105 | }
106 | 
107 | exports['first test'] = function(test) { ... };
108 | exports['second test'] = function(test) { ... };
109 | exports['third test'] = function(test) { ... };
110 | 
111 | 112 |

113 | Now, you can tell node-async-testing to run the tests in parallel: 114 |

115 | 116 |
node test-suite.js --parallel
117 | 118 |

119 | Or to only run specific tests: 120 |

121 | 122 |
node test-suite.js --test-name "first test" --test-name "third test"
123 | 124 |

125 | Use the help flag to see all the options: 126 |

127 | 128 |
node test-suite.js --help
129 | 130 |

131 | node-async-testing can run multiple files at once this way, because 132 | additional files will get passed with process.ARGV: 133 |

134 | 135 |
node test-suite.js test-suite2.js
136 | 137 |

138 | For example, you can run all the tests in a test directory by saying: 139 |

140 | 141 |
node test/*
142 | 143 |

144 | With this arrangement, the exit code of the process will be the number of tests 145 | that didn't succeed. 146 |

147 | 148 |

149 | node-async-testing comes with a "web" test runner. This runner launches a 150 | web server which can be used to run suites manually. Launch it with the 151 | --web flag: 152 |

153 | 154 |
node test/* --web
155 | 156 |

157 | Once the server is started, in a browser you can pick and choose which suites 158 | to run, and run them as many times as you like. node-async-testing reloads 159 | the suites (and any code they use) from scratch each time they are run so you 160 | can leave the web server running and switch back and forth between editing tests 161 | or code and running the tests. Very handy! 162 |

163 | 164 |

165 | To use the web runner you also need to install 166 | Socket.IO. 167 |

168 | 169 |
npm install socket.io
170 | 171 |

172 | [The server is known to work in the lastest versions of Safari, Chrome and 173 | Firefox. Any help in getting it to work in Opera would be much appreciated. I 174 | don't have means of testing in IE, so I can't tell you how it performs there.] 175 |

176 | 177 |
178 |

Running suites using the API

179 |

180 | If you want to organize your suites in a different manner (and say not have them 181 | organized by file), or don't like the included test runners, you are going to 182 | have to run your suites manually or write your own runner. 183 |

184 | 185 |

186 | node-async-testing comes with the following functions for running suites 187 | and test runners: 188 |

189 | 190 | 197 | 198 |
199 |
runSuite(suiteObject, options)
200 |
201 |

202 | The runSuite function is the heart of node-async-testing. 203 | It receives two arguments. 204 |

205 | 206 |

207 | The first argument is the actual suite that you want to run. See 208 | Writing Tests for the details of writing 209 | a suite. 210 |

211 | 212 |

213 | The second argument is an object with options for running the suite. 214 | This object can have the following properties: 215 |

216 | 217 |
218 |
name
219 |
220 | This is the name of the test suite being run. This is optional, as it 221 | doesn't affect the running of the suite at all. If it is provided it 222 | is passed to the onSuiteStart event callback. 223 |
224 |
parallel
225 |
226 | If this property is present and “truethy”, then the tests 227 | will be run in parallel. Otherwise, runSuite won't start 228 | another test until the previous one has completely finished. 229 |
230 |
testName
231 |
232 | This should be either a String or an Array of Strings. If this property 233 | is present then only those tests whose names match it will be ran. Use 234 | this to only run specific tests. 235 |
236 |
237 | 238 |

239 | In addition to those properties, the options in the 240 | events callbacks section are supported. 241 |

242 | 243 |

244 | runSuite adds a listener to the process for uncaught exceptions, 245 | and as such, there should be no other code running while runSuite 246 | is doing its thing, otherwise the other code could interfere with the 247 | suite being ran. 248 |

249 |
250 | 251 |
runFile(suitePath, options)
252 |
253 |

254 | The runFile function is similar to runSuite except instead of 255 | running the suite in the main process, it opens up a child process and runs it 256 | there. The benefits of this are that it is able to report on syntax errors, and 257 | also be run while other suites are running. 258 |

259 |

260 | The first argument is a module path to the suite for Node's 261 | require() function. 262 |

263 |

264 | The second argument is an object with options for running the suite. It is practically 265 | the same as the second argument for runSuite, except it has one for additional 266 | event callback. See the events callbacks section for 267 | the full list. 268 |

269 | 270 |
expandFiles(list, [filter], callback)
271 |
272 |

273 | The expandFiles function takes a list of files and or 274 | directories and returns a list of just files. It recursively searches 275 | through any directories for files that begin with test-. It 276 | is useful for expanding a user supplied list of files and directories. It 277 | takes three properties: 278 |

279 | 280 |
    281 |
  1. 282 | A String or an Array of Strings, the list of files and directories to 283 | expand. 284 |
  2. 285 |
  3. 286 | A String or an Array of Strings, a list of file names by which you want 287 | to filter the found files. This makes it so you can specify specific 288 | file names that you want to find. This is optional. 289 |
  4. 290 |
  5. 291 | Callback, which will get called with the found files when 292 | expandFiles is done. 293 |
  6. 294 |
295 | 296 |

297 | It returns an array of objects which have the following properties: 298 |

299 | 300 |
301 |
name
302 |
This is the file name that was passed in.
303 |
path
304 |
This is an absolute module path to the file that can be require()ed.
305 |
306 |
307 | 308 |
run()
309 |
310 |

311 | The run functions is talked about at length in the 312 | Running Suite Files section. It handles 313 | outputing the results of suites for you, so you don't have to worry about it. 314 | node-async-testing comes with two built-in runners, one for consoles and one 315 | for web browsers. 316 |

317 | 318 |

319 | run can take any number of arguments, which can be any 320 | one of: 321 |

322 | 323 |
    324 |
  • 325 | A string, the name of a file to run. The exports object of the file should be 326 | a suite object. 327 |
  • 328 |
  • 329 | An array of command line arguments, like process.ARGV. 330 |
  • 331 |
  • 332 | An options object, for manipulating the options for the function directly. The 333 | options you can set here correspond directly to the ones you can set in via the 334 | command line. 335 |
  • 336 |
  • 337 |

    338 | The last argument can be a callback which will get called when all the specified 339 | suites have finished running. The callback will receive one argument, an array 340 | with an object for each suite, which has the name, status and suite result object 341 | for that suite. 342 |

    343 |

    344 | If a callback is not supplied, node-async-testing assumes that you don't 345 | want to do anything after the suites have completed, so the run 346 | function creates its own callback which exits the process and sets the exit 347 | code to the number of suites that didn't complete successfully. 348 |

    349 |
  • 350 |
351 | 352 | 353 |

354 | The order matters, latter settings override earlier ones. Here are some examples: 355 |

356 | 357 |

Run a file:

358 |
async_testing.run('myFile.js');
359 | 360 |

Change options:

361 |
async_testing.run({parallel: true}, 'myFile.js');
362 | 363 | 364 |

Use command line arguments:

365 |
async_testing.run(process.ARGV);
366 | 367 |

Use command line arguments with defaults:

368 |
async_testing.run({parallel: true}, process.ARGV);
369 | 370 |

Overwrite command line arguments:

371 |
async_testing.run(process.ARGV, {parallel: true});
372 |
373 | 374 |
registerRunner(modulePath, [default])
375 |
376 |

377 | Use this function to add your own custom runners to node-async-testing. 378 | See lib/console-runner.js 379 | or lib/web-runner.js 380 | for examples of how to write a runner. 381 |

382 |

383 | The first argument is the path to the runner which you are registering. 384 | The second variable is for whether or not you want this to be the 385 | default runner. 386 |

387 |
388 | 389 |
390 |
391 | 392 |
393 |

Description of event callbacks

394 |

395 | The runSuite and runFile functions can be given event callbacks for outputing 396 | the results of the tests. Using these callbacks it is possible to write your 397 | own test runners and format the output however you'd like. These callbacks 398 | are not for manipulating tests, but purely for reporting. 399 |

400 | 401 |

Events

402 | 403 |
404 |
onTestStart(testName)
405 |
Called when a test is started. 406 |
407 | 408 |
onTestDone(status, testResult)
409 |
410 | Called when a test finishes. See Test result 411 | below. 412 | status is one of the following:
failure or success. 413 |
414 | 415 |
onSuiteDone(status, suiteResult)
416 |
417 | Called when a suite finishes. See Suite result 418 | below. 419 | status is one of the following:
complete, error, 420 | exit or loadError. 421 |
422 |
423 | 424 |

Suite Result

425 |

426 | A suite result is an object that looks like one of the following, depending 427 | on what the finish status of the suite was: 428 |

429 | 430 |
431 |
complete
432 |
433 |

434 | occures when all tests finished running, and node-async-testing 435 | was able to accurately determine how each one finished. 436 |

437 |
438 | { tests: an Array of test results for each test that completed
439 | , numFailures: the number of tests that failed
440 | , numSuccesses: the number of tests that passed
441 | }
442 |
443 | 444 |
error
445 |
446 |

447 | occures when an uncaught error is thrown from a test and node-async-testing 448 | can't determine which test caused it. When this happens, node-async-testing 449 | stops running the suite and exits the process. 450 |

451 |
452 | { error: the error object that was thrown
453 | , tests: an Array of the names of each test that could have caused the error
454 | }
455 |
456 | 457 |
exit
458 |
459 |

460 | occures when the process running the suite exits and there are still tests that 461 | haven't finished. This occurs when people forget to finish their tests or their 462 | tests don't work like they expected. 463 |

464 |
465 | { tests: an Array of the names of each test that didn't finish
466 | }
467 |
468 | 469 |
loadError
470 |
471 |

472 | this type of result is only produced from runFile, and 473 | occures when the child process can't load the suite. 474 |

475 |
476 | { stderr: what was written to <stderr> before the child process exited
477 | }
478 |
479 |
480 | 481 |

Test Result

482 |

483 | A test result is an object that looks like one of the following, depending on 484 | what the finish status of the test was: 485 |

486 | 487 |
488 |
success
489 |
490 |

the test completed successfully

491 |
492 | { name: test name
493 | , numAssertions: number of assertions completed successfully
494 | }
495 |
496 | 497 |
failure
498 |
499 |

the test failed in some way

500 |
501 | { name: test name
502 | , failureType: 'assertion' or 'error' depending on how this failed
503 | , failure: the error that caused this to fail
504 | }
505 |
506 |
507 |
508 |
509 | 510 | 511 | -------------------------------------------------------------------------------- /docs/writing-tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | node-async-testing — simple intuitive testing for node.js 6 | 7 | 8 | 9 | 10 |
11 |
12 |

node-async-testing

13 |

Simple, intuitive testing for node.js

14 |
15 | 16 | 23 | 24 |
25 |

Writing Test Suites

26 | 27 |

28 | The hard part of writing a test suite for asynchronous code is figuring out 29 | when a test is done, and feeling confident that all the code was run. 30 |

31 | 32 |

33 | node-async-testing addresses that by... 34 |

35 | 36 |
    37 |
  1. giving each test its own unique assert object. This way you know which 38 | assertions correspond to which tests. 39 |
  2. 40 |
  3. allowing you to run the tests one at a time. This way it is possible to 41 | add a global exception handler to the process and know exactly which test 42 | cause an error. 43 |
  4. 44 |
  5. requiring you to tell the test runner when the test is finished. This way 45 | you don't have any doubt as to whether or not an asynchronous test still 46 | has code to run. 47 |
  6. 48 |
  7. allowing you to declare how many assertions should take place in a test. 49 | This way you can ensure that your callbacks aren't being called too many 50 | or too few times. 51 |
  8. 52 |
53 | 54 |

55 | All of the examples in this page can be seen in the 56 | test/test-overview.js 57 | suite and can be executed by running the following command from within the 58 | node-async-testing directory: 59 |

60 | 61 |
node test/test-overview.js
62 | 63 |

Page outline:

64 | 83 |
84 | 85 |
86 |

Tests

87 |

88 | node-async-testing tests are just a functions that take a ‘test object’: 89 |

90 | 91 |
 92 | function asynchronousTest(test) {
 93 |   setTimeout(function() {
 94 |     // make an assertion (these are just regular Node assertions)
 95 |     test.ok(true);
 96 | 
 97 |     // finish the test
 98 |     test.finish();
 99 |   });
100 | }
101 | 102 |

103 | The test object is where all the action takes place. You make your assertions 104 | using this object (test.ok(), test.deepEquals(), 105 | etc) and use it to finish the test (test.finish()). Basically, 106 | all the actions that are directly related to a test use this object. 107 |

108 | 109 |

110 | Test objects have the following properties: 111 |

112 | 113 |
114 |
test.finish()
115 |
116 |

117 | node-async-testing assumes all tests are asynchronous. So in 118 | order for it to know that a given test has completed, you have to 119 | ‘finish’ the test by calling test.finish(). 120 | This let's the test runner know that that particular test doesn't have 121 | any more asynchronous code running. 122 |

123 | 124 |

125 | Even if a test is not asynchronous you still have to finish it: 126 |

127 | 128 |
129 |     function synchronousTest(test) {
130 |       test.ok(true);
131 |       test.finish();
132 |     }
133 | 134 |

Tip:

135 |

It is important that no code from this test is ran after 136 | this function is called, otherwise, node-async-testing will think 137 | that code corresponds to a different test. Be careful! 138 |

139 | 140 |

141 | If test.finish() is called more than once, or an assertion is made after finish() 142 | has already been called, an error will be thrown. These features help catch test 143 | errors. Use these to your advantage. For example, at the beginning of an asynchronous 144 | callback make an assertion! If the test has already finished (the callback is being called 145 | when it isn't supposed to be) then the assertion will catch that the test has already finished 146 | and prevent the callback from running any code that can interfere with other tests. Example: 147 |

148 | 149 |
150 | module.exports =
151 |   { 'okay': function(test) {
152 |       setTimeout(function() {
153 |         /* run code */
154 |         test.finish();
155 |       }
156 |     }
157 |   , 'GOOD PRACTICE!': function(test) {
158 |       setTimeout(function() {
159 |         test.ok(true); // make sure test isn't already finished
160 |         
161 |         /* run code */
162 | 
163 |         test.finish();
164 |       }
165 |     }
166 |   };
167 |
168 | 169 |
test.numAssertions
170 |
171 |

172 | node-async-testing lets you be explicit about the number of 173 | assertions run in a given test: set numAssertions on the test 174 | object. This can be very helpful in asynchronous tests where you want to 175 | be sure all callbacks get fired: 176 |

177 | 178 |
179 |     suite['test assertions expected'] = function(test) {
180 |       test.numAssertions = 1;
181 | 
182 |       test.ok(true);
183 |       test.finish();
184 |     }
185 |

186 | If you are testing asynchronous code, I highly recommend using test.numAssertions. 187 | node-async-testing depends on test.finish() to tell it when a test is finished, 188 | so if code is run after finish() was called you will get innacurate test results. Use 189 | numAssertions to be sure all your callbacks are called and that they aren't called 190 | too many times. 191 |

192 |
193 | 194 |
test.uncaughtExceptionHandler
195 |
196 |

197 | node-async-testing lets you deal with uncaught errors. If you 198 | expect an error to be thrown asynchronously in your code somewhere (this 199 | is not good practice, but sometimes when using other people's code you 200 | have no choice. Or maybe it is what you want to happen, who am I 201 | to judge?), you can set an uncaughtExceptionHandler on the 202 | test object: 203 |

204 | 205 |
206 |     suite['test catch async error'] = function(test) {
207 |       var e = new Error();
208 | 
209 |       test.uncaughtExceptionHandler = function(err) {
210 |         test.equal(e, err);
211 |         test.finish();
212 |       }
213 | 
214 |       setTimeout(function() {
215 |           throw e;
216 |         }, 500);
217 |     };
218 | 219 |

220 | This property can only be set when running suites serially, because 221 | otherwise node-async-testing wouldn't know for which test it 222 | was catching the error. 223 |

224 |
225 |
226 | 227 |

Assertions

228 |

229 | Additionally, the test object has all of the assertion functions that 230 | come with the assert module 231 | bundled with Node. 232 |

233 | 234 |

235 | These methods are bound to the test object so node-async-testing 236 | can know which assertions correspond to which tests. 237 |

238 | 239 |
240 |
test.ok(value, [message])
241 |
Tests if value is a true value, it is equivalent to assert.equal(true, value, message);.
242 | 243 |
test.equal(actual, expected, [message])
244 |
Tests shallow, coercive equality with the equal comparison operator ( == ).
245 | 246 |
test.notEqual(actual, expected, [message])
247 |
Tests shallow, coercive non-equality with the not equal comparison operator ( != ).
248 | 249 |
test.deepEqual(actual, expected, [message])
250 |
Tests for deep equality.
251 | 252 |
test.notDeepEqual(actual, expected, [message])
253 |
Tests for any deep inequality.
254 | 255 |
test.strictEqual(actual, expected, [message])
256 |
Tests strict equality, as determined by the strict equality operator ( === )
257 | 258 |
test.notStrictEqual(actual, expected, [message])
259 |
Tests strict non-equality, as determined by the strict not equal operator ( !== )
260 | 261 |
test.throws(block, [error], [message])
262 |
Expects block to throw an error.
263 | 264 |
test.doesNotThrow(block, [error], [message])
265 |
Expects block not to throw an error.
266 | 267 |
test.ifError(value, [message])
268 |
Tests if value is not a false value, throws if it is a true value. Useful when testing the first argument, error in callbacks.
269 |
270 | 271 |

Custom assertions

272 |

273 | Because node-async-testing needs to bind the assertion methods to the 274 | test object, you can't just use any assertion you have lying around. You have to 275 | first register it: 276 |

277 | 278 |
279 | var async_testing = require('async_testing');
280 | async_testing.registerAssertion('customAssertion', function() { ... });
281 | 
282 | exports['test assert'] = function(test) {
283 |   test.customAssertion();
284 |   test.finish();
285 | }
286 | 287 |

288 | Check out 289 | test/test-custom_assertions.js 290 | for a working example. 291 |

292 | 293 |
294 | 295 |
296 |

Suites

297 |

298 | node-async-testing is written for running suites of tests, not individual 299 | tests. A test suite is just an object with test functions: 300 |

301 | 302 |
303 | var suite = {
304 |   'asynchronous test': function(test) {
305 |     setTimeout(function() {
306 |       test.ok(true);
307 |       test.finish();
308 |     });
309 |   },
310 |   'synchronous test': function(test) {
311 |     test.ok(true);
312 |     test.finish();
313 |   }
314 | }
315 | 316 |

Sub-suites

317 |

318 | node-async-testing allows you to namespace your tests by putting them 319 | in a sub-suite: 320 |

321 | 322 |
323 | var suite =
324 |   { 'namespace 1':
325 |     { 'test A': function(test) { ... }
326 |     , 'test B': function(test) { ... }
327 |     }
328 |   , 'namespace 2':
329 |     { 'test A': function(test) { ... }
330 |     , 'test B': function(test) { ... }
331 |     }
332 |   , 'namespace 3':
333 |     { 'test A': function(test) { ... }
334 |     , 'test B': function(test) { ... }
335 |     }
336 |   };
337 | 338 |

339 | Suites can be nested arbitrarily deep: 340 |

341 | 342 |
343 | var suite =
344 |   { 'namespace 1':
345 |     { 'namespace 2':
346 |       { 'namespace 3':
347 |         { 'test A': function(test) { ... }
348 |         , 'test B': function(test) { ... }
349 |         , 'test C': function(test) { ... }
350 |         }
351 |       }
352 |     }
353 |   };
354 | 355 |

wrap()

356 |

357 | node-async-testing comes with a convenience function for wrapping all tests 358 | in an object with setup/teardown functions. This function is called wrap 359 | and it takes one argument which is an object which can have the following properties: 360 |

361 | 362 |
363 |
suite
364 |
This property is required and should be the suite object you want to wrap.
365 | 366 |
setup(test, done)
367 |
368 | This function is run before every single test in the suite. The first 369 | argument is the test object for which this setup is being run. If you want 370 | to pass additional data/objects to the test, you should set them on the 371 | test object directly. Because setup might need 372 | to be asynchronous, you have to call the second argument, done(), 373 | when it is finished. Optional. 374 |
375 | 376 |
teardown(test, done)
377 |
378 | This function is run after every single test in the suite. teardown 379 | functions are called regardless of whether or not the test succeeds or 380 | fails. It gets the same arguments as setup. Optional. 381 |
382 | 383 |
suiteSetup(done)
384 |
385 | This function is run once before any test in the suite. Because 386 | suiteSetup might need to be asynchronous, you have to call the 387 | only argument, done(), when it is finished. Optional. 388 |
389 | 390 |
suiteTeardown(done)
391 |
392 | This function is run once after all tests in the suite have 393 | finished. suiteTeardown 394 | functions are called regardless of whether or not the tests in the suite 395 | succeed or fail. It gets the same arguments as suiteSetup. 396 | Optional. 397 |
398 |
399 | 400 |

An example:

401 | 402 |
403 | var wrap = require('async_testing').wrap;
404 | 
405 | var suiteSetupCount = 0;
406 | 
407 | var suite = wrap(
408 |   { suiteSetup: function(done) {
409 |       suiteSetupCount++;
410 |       done();
411 |     }
412 |   , setup: function(test, done) {
413 |       test.extra1 = 1;
414 |       test.extra2 = 2;
415 |       done();
416 |     }
417 |   , suite:
418 |     { 'wrapped test 1': function(test) {
419 |         test.equal(1, suiteSetupCount);
420 |         test.equal(1, test.extra1);
421 |         test.equal(2, test.extra2);
422 |         test.finish();
423 |       }
424 |     , 'wrapped test 2': function(test) {
425 |         test.equal(1, suiteSetupCount);
426 |         test.equal(1, test.extra1);
427 |         test.equal(2, test.extra2);
428 |         test.finish();
429 |       }
430 |     }
431 |   , teardown: function(test, done) {
432 |       // not that you need to delete these variables here, they'll get cleaned up
433 |       // automatically, we're just doing it here as an example of running code
434 |       // after some tests
435 |       delete test.extra1;
436 |       delete test.extra2;
437 |       done();
438 |     }
439 |   , suiteTeardown: function(done) {
440 |       delete suiteSetupCount;
441 |       done();
442 |     }
443 |   })
444 | })
445 | 446 |

447 | You can use a combination of sub-suites and wrapping to provide setup/teardown 448 | functions for certain tests. 449 |

450 |
451 | 452 |
453 | 454 | 455 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // convenience file for easily including async testing 2 | module.exports = require('./lib/async_testing'); 3 | -------------------------------------------------------------------------------- /lib/async_testing.js: -------------------------------------------------------------------------------- 1 | var running = require('./running'); 2 | exports.run = running.run; 3 | exports.registerRunner = running.registerRunner; 4 | 5 | var testing = require('./testing'); 6 | exports.runSuite = testing.runSuite; 7 | exports.runFile = testing.runFile; 8 | exports.expandFiles = testing.expandFiles; 9 | exports.registerAssertion = testing.registerAssertion; 10 | 11 | var wrap = require('./wrap'); 12 | exports.wrap = wrap.wrap; 13 | -------------------------------------------------------------------------------- /lib/child.js: -------------------------------------------------------------------------------- 1 | 2 | //this module SHOULD NOT be required as a library, it will interfere. 3 | //must be used in a spawned process. 4 | if (module !== require.main) { 5 | return 6 | } 7 | 8 | var testing = require('./testing'); 9 | 10 | var opts = 11 | { parallel: JSON.parse(process.ARGV[3]) 12 | , testName: JSON.parse(process.ARGV[4]) 13 | , onTestStart: function testStart(name) { 14 | postMessage('onTestStart', name); 15 | } 16 | , onTestDone: function testDone(status, result) { 17 | if (result.failure) { 18 | result.failure = makeErrorJsonable(result.failure); 19 | } 20 | 21 | postMessage('onTestDone', status, result); 22 | } 23 | , onSuiteDone: function suiteDone(status, results) { 24 | postMessage('onSuiteDone', status, results); 25 | } 26 | }; 27 | 28 | var s = require(process.ARGV[2]); 29 | 30 | testing.runSuite(s, opts); 31 | 32 | 33 | function postMessage() { 34 | console.log("\n" + testing.messageEncode.apply(null, arguments)); //hack for interference problem. just prepend a newline. 35 | //means that output is a few newlines, and never completely clean :( but behaves better. 36 | } 37 | 38 | function makeErrorJsonable(err) { 39 | var r = new RegExp(process.cwd(),'g') 40 | return { 41 | message: err.message || null 42 | , stack: err.stack ? err.stack.replace(r, '.') : '[no stack trace]' 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/console-runner.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , path = require('path') 3 | ; 4 | 5 | exports.name = 'Console'; 6 | 7 | exports.runnerFlag = 8 | { description: 'use the console runner for running tests from a terminal' 9 | , longFlag: 'console' 10 | , key: 'runner' 11 | , value: 'Console' 12 | , shortFlag: 'c' 13 | }; 14 | 15 | exports.optionsFlags = 16 | [ { longFlag: 'log-level' 17 | , shortFlag: 'l' 18 | , key: 'verbosity' 19 | , description: '0 => succinct, 1 => default, 2 => full stack traces' 20 | , takesValue: 'level' 21 | } 22 | , { shortFlag: '0' 23 | , key: 'verbosity' 24 | , value: 0 25 | , description: 'set log level to 0' 26 | } 27 | , { shortFlag: '1' 28 | , key: 'verbosity' 29 | , value: 1 30 | , description: 'set log level to 1' 31 | } 32 | , { shortFlag: '2' 33 | , key: 'verbosity' 34 | , value: 2 35 | , description: 'set log level to 2' 36 | } 37 | , { longFlag: 'all' 38 | , shortFlag: 'a' 39 | , key: 'printSuccesses' 40 | , description: 'don\'t supress information about passing tests' 41 | } 42 | , { longFlag: 'no-color' 43 | , description: 'don\'t use colored output' 44 | } 45 | ]; 46 | 47 | var testing = require('./async_testing'); 48 | 49 | /* The defualt test runner 50 | * 51 | * list: an array of filenames or modules to be run, or a commonjs module 52 | * options: options for running the suites 53 | * callback: a function to be called then the suites are finished 54 | */ 55 | exports.run = function(list, options, callback) { 56 | 57 | var red = function(str){return "\033[31m" + str + "\033[39m"} 58 | , yellow = function(str){return "\033[33m" + str + "\033[39m"} 59 | , green = function(str){return "\033[32m" + str + "\033[39m"} 60 | , bold = function(str){return "\033[1m" + str + "\033[22m"} 61 | ; 62 | 63 | if (!('verbosity' in options)) { 64 | options.verbosity = 1; 65 | } 66 | 67 | // clean up and parse options 68 | if (options.noColor) { 69 | red = green = yellow = function(str) { return str; }; 70 | } 71 | 72 | var suites 73 | , startTime 74 | , index = 0 75 | , finishedIndex = 0 76 | , results = [] 77 | ; 78 | 79 | testing.expandFiles(list, options.suiteName, startSuites); 80 | 81 | function startSuites(err, list) { 82 | if (err) { 83 | throw err; 84 | } 85 | suites = list; 86 | 87 | for (var i = 0; i < suites.length; i++) { 88 | suites[i].index = i; 89 | suites[i].printedName = false; 90 | suites[i].queuedTestResults = []; 91 | } 92 | 93 | startTime = new Date(); 94 | startNextSuite(); 95 | } 96 | function startNextSuite() { 97 | if (index >= suites.length) { 98 | // no more to start 99 | return; 100 | } 101 | 102 | var suite = suites[index]; 103 | index++; 104 | 105 | var opts = 106 | { parallel: options.testsParallel 107 | , testName: options.testName 108 | , onTestDone: function(status, result) { 109 | testFinished(suite, status, result); 110 | } 111 | , onSuiteDone: function(status, results) { 112 | suiteFinished(suite, status, results); 113 | } 114 | } 115 | 116 | suite.startTime = new Date(); 117 | testing.runFile(suite.path, opts); 118 | 119 | if (options.suitesParallel) { 120 | startNextSuite(); 121 | } 122 | } 123 | 124 | // we want to output the suite results in the order they were given to us, 125 | // so as suties finish we buffer them, and then output them in the right 126 | // order when we can 127 | function suiteFinished(suite, status, results) { 128 | suite.finished = true; 129 | suite.status = status; 130 | suite.duration = new Date() - suite.startTime; 131 | suite.results = results; 132 | delete suite.startTime; 133 | 134 | if (suite.index == finishedIndex) { 135 | for (var i = finishedIndex; i < suites.length; i++) { 136 | if (suites[i].finished) { 137 | while(suites[i].queuedTestResults.length) { 138 | output.testDone(suites[i], suites[i].queuedTestResults.shift()); 139 | } 140 | output.suiteDone(suites[i]); 141 | } 142 | else { 143 | break; 144 | } 145 | } 146 | finishedIndex = i; 147 | } 148 | 149 | if (finishedIndex >= suites.length) { 150 | output.allDone(); 151 | } 152 | 153 | startNextSuite(); 154 | } 155 | 156 | function testFinished(suite, status, result) { 157 | suite.queuedTestResults.push(result); 158 | if (suite.index == finishedIndex) { 159 | while(suite.queuedTestResults.length) { 160 | output.testDone(suite, suite.queuedTestResults.shift()); 161 | } 162 | } 163 | } 164 | 165 | var output = 166 | { testDone: function(suite, result) { 167 | if (!suite.printedName) { 168 | if (options.printSuccesses || result.failure) { 169 | if (suite.name) { 170 | console.log(bold(suite.name)); 171 | } 172 | suite.printedName = true; 173 | } 174 | } 175 | 176 | if (result.failure) { 177 | var color = result.failureType == 'assertion' ? red : yellow; 178 | console.log(color(' ✖ ' + result.name)); 179 | } 180 | else if (options.printSuccesses) { 181 | console.log(' ✔ ' + result.name); 182 | } 183 | } 184 | , suiteDone: function(suite) { 185 | output['suite'+suite.status.substr(0,1).toUpperCase()+suite.status.substr(1)](suite); 186 | } 187 | , suiteLoadError: function(suite) { 188 | var err = suite.results.stderr; 189 | 190 | if (!suite.printedName) { 191 | console.log(bold(suite.name)); 192 | } 193 | 194 | console.log(yellow(' ' + bold('!') + ' Error loading suite')); 195 | 196 | if (options.verbosity > 0) { 197 | console.log(''); 198 | 199 | var lines = err.split('\n'); 200 | 201 | var num = options.verbosity == 1 ? Math.min(6, lines.length) : lines.length; 202 | for (var i = 0; i < num; i++) { 203 | console.log(' ' + lines[i]); 204 | } 205 | } 206 | 207 | console.log(''); 208 | } 209 | , suiteComplete: function(suite) { 210 | var suiteResults = suite.results; 211 | var tests = suiteResults.tests; 212 | 213 | if (tests.length == 0) { 214 | return; 215 | } 216 | 217 | var last = ''; 218 | if(options.verbosity > 0) { 219 | if (suites.length > 1 || suiteResults.numFailures > 0) { 220 | 221 | if (!suite.printedName) { 222 | console.log(bold(suite.name)); 223 | } 224 | } 225 | 226 | if (options.printSuccesses || suiteResults.numFailures > 0) { 227 | console.log(''); 228 | } 229 | 230 | var totalAssertions = 0; 231 | 232 | for(var i = 0; i < tests.length; i++) { 233 | var r = tests[i]; 234 | if (r.failure) { 235 | var s = r.failure.stack.split("\n"); 236 | 237 | console.log(r.failureType == 'assertion' ? ' Failure: '+red(r.name) : ' Error: '+yellow(r.name)); 238 | 239 | if (r.failure.message) { 240 | console.log(' '+ r.failure.message); 241 | } 242 | 243 | if (options.verbosity == 1) { 244 | if (s.length > 1) { 245 | console.log(s[1].replace(process.cwd(), '.')); 246 | } 247 | if (s.length > 2) { 248 | console.log(s[2].replace(process.cwd(), '.')); 249 | } 250 | } 251 | else { 252 | for(var k = 1; k < s.length; k++) { 253 | console.log(s[k].replace(process.cwd(), '.')); 254 | } 255 | } 256 | } 257 | else { 258 | totalAssertions += r.numAssertions; 259 | } 260 | } 261 | 262 | var total = suiteResults.numFailures+suiteResults.numSuccesses; 263 | 264 | if (suiteResults.numFailures > 0) { 265 | console.log(''); 266 | last += ' '; 267 | if (suites.length > 1) { 268 | last += ' FAILURES: '+suiteResults.numFailures+'/'+total+' tests failed.'; 269 | } 270 | } 271 | else if (suites.length > 1) { 272 | last += ' '+green('OK: ')+total+' test'+(total == 1 ? '' : 's')+'. '+totalAssertions+' assertion'+(totalAssertions == 1 ? '' : 's')+'.'; 273 | } 274 | } 275 | 276 | if (options.verbosity == 0) { 277 | if (options.printSuccesses || suiteResults.numFailures > 0) { 278 | console.log(''); 279 | } 280 | } 281 | else if(last.length && suites.length > 1) { 282 | console.log(last + ' ' + (suite.duration/1000) + ' seconds.'); 283 | console.log(''); 284 | } 285 | } 286 | , suiteError: function(suite) { 287 | var err = suite.results.error; 288 | var tests = suite.results.tests; 289 | 290 | if (!suite.printedName && suite.name) { 291 | console.log(bold(suite.name)); 292 | } 293 | 294 | var names = tests.slice(0, tests.length-1).map(function(name) { return yellow(name); }).join(', ') + 295 | (tests.length > 1 ? ' or ' : '') + 296 | yellow(tests[tests.length-1]); 297 | 298 | console.log(yellow(' ✖ ') + names); 299 | 300 | if (options.verbosity > 0) { 301 | console.log(''); 302 | 303 | if (tests.length > 1) { 304 | console.log(' Stopped Running suite! Cannot determine which test threw error: '); 305 | console.log(' ' + names); 306 | } 307 | else { 308 | console.log(' Error: ' + yellow(tests[0])); 309 | } 310 | 311 | var s = err.stack.split("\n"); 312 | if (err.message) { 313 | console.log(' '+err.message); 314 | } 315 | if (options.verbosity == 1) { 316 | if (s.length > 1) { 317 | console.log(s[1].replace(process.cwd(), '.')); 318 | } 319 | if (s.length > 2) { 320 | console.log(s[2].replace(process.cwd(), '.')); 321 | } 322 | } 323 | else { 324 | for(var k = 1; k < s.length; k++) { 325 | console.log(s[k]); 326 | } 327 | } 328 | } 329 | 330 | console.log(''); 331 | } 332 | , suiteExit: function(suite) { 333 | // TODO: test me 334 | var tests = suite.results.tests; 335 | 336 | console.log(''); 337 | console.log('Process exited. The following test'+(tests.length == 1 ? '' : 's')+' never finished:'); 338 | 339 | console.log(''); 340 | for(var i = 0; i < tests.length; i++) { 341 | console.log(' + '+tests[i]); 342 | } 343 | console.log(''); 344 | 345 | console.log('Did you forget to call test.finish()?'); 346 | } 347 | , allDone: function() { 348 | var passingSuites = 0 349 | , totalSuites = 0 350 | , totalTests = 0 351 | , passingTests = 0 352 | ; 353 | 354 | for(var i = 0; i < suites.length; i++) { 355 | totalSuites++; 356 | if (suites[i].status == 'complete' && suites[i].results.numFailures == 0) { 357 | passingSuites++; 358 | } 359 | 360 | if ('numSuccesses' in suites[i].results) { 361 | totalTests += suites[i].results.numSuccesses + suites[i].results.numFailures; 362 | passingTests += suites[i].results.numSuccesses; 363 | } 364 | else { 365 | totalTests += NaN; 366 | } 367 | } 368 | 369 | var last = ''; 370 | if (passingSuites != totalSuites) { 371 | last += red('PROBLEMS:'); 372 | last += ' '+(totalSuites-passingSuites)+'/'+totalSuites+' suites had problems.'; 373 | } 374 | else { 375 | last += green('SUCCESS:'); 376 | last += ' '+totalSuites+' suite' + (totalSuites == 1 ? '' : 's') + '.'; 377 | } 378 | 379 | if (isNaN(totalTests)) { 380 | last += ' Could not count tests.'; 381 | } 382 | else if (passingTests != totalTests) { 383 | last += ' ' + (totalTests-passingTests)+'/'+totalTests+' tests' + ' failed.'; 384 | } 385 | else { 386 | last += ' ' + totalTests + ' test' + (totalTests == 1 ? '' : 's') + '.'; 387 | } 388 | 389 | console.log(bold(last + ' ' + ((new Date() - startTime)/1000) + ' seconds.')); 390 | 391 | if (callback) { 392 | var allResults = suites.map(function(s) { 393 | return { 394 | name: s.name 395 | , status: s.status 396 | , results: s.results 397 | }; 398 | }); 399 | callback(allResults); 400 | } 401 | } 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /lib/running.js: -------------------------------------------------------------------------------- 1 | // holds the available runners 2 | var runners = {}; 3 | // keeps track of the default 4 | var defRunner; 5 | 6 | var flags = {}; 7 | flags['Behavior'] = 8 | [ { longFlag: 'test-name' 9 | , shortFlag: 't' 10 | , multiple: true 11 | , description: 'only run tests with the specified name' 12 | , takesValue: 'name' 13 | } 14 | , { longFlag: 'suite-name' 15 | , multiple: true 16 | , shortFlag: 's' 17 | , description: 'only run suites with the specified name' 18 | , takesValue: 'name' 19 | } 20 | , { longFlag: 'parallel' 21 | , shortFlag: 'p' 22 | , takesValue: 'what' 23 | , options: ['both', 'neither', 'tests', 'suites'] 24 | , def: 'both' 25 | , description: 'what to run in parallel' 26 | } 27 | , { longFlag: 'help' 28 | , shortFlag: 'h' 29 | , description: 'output this help message and exit' 30 | } 31 | ]; 32 | 33 | /* Allow people to add their own runners 34 | * 35 | * A runner should export 4 things: 36 | * 37 | * name: The name of the runner 38 | * run: The run function 39 | * runnerFlag: The options for the command line flag used to choose the runner 40 | * optionsFlags: The options for the command line flags used to configure this runner 41 | * 42 | * See lib/console-runner.js or lib/web-runner.js for examples on what these 43 | * should look like 44 | */ 45 | exports.registerRunner = function(p, def) { 46 | var m = require(p); 47 | 48 | // TODO check to make sure we have everything we need 49 | 50 | var r = runners[m.name] = 51 | { module: m 52 | , name: m.name 53 | , runnerFlag: m.runnerFlag 54 | , optionsFlags: m.optionsFlags 55 | , path: p 56 | }; 57 | 58 | if (def) { 59 | defRunner = m.name; 60 | } 61 | 62 | flags['Behavior'].push(r.runnerFlag); 63 | flags[m.name + ' Runner'] = r.optionsFlags; 64 | } 65 | 66 | // add the built in runners 67 | exports.registerRunner('./console-runner', true); 68 | exports.registerRunner('./web-runner'); 69 | 70 | /* For running a bunch of tests suites and outputting results 71 | * 72 | * Takes 3 arguments: 73 | * list: file or directory or array of files or directories 74 | * args: array of command line arguments to change settings. this can also 75 | * have file names in it that should be run 76 | * callback: a callback for when it has run all suites. If this is not 77 | * provided, when all suites have finished this will call 78 | * process.exit with a status code equal to the number of tests that 79 | * failed. If you don't need a callback but don't want it to do 80 | * that, then pass something falsey 81 | */ 82 | exports.run = function() { 83 | var args = Array.prototype.slice.call(arguments) 84 | , cb 85 | ; 86 | 87 | if (typeof args[args.length-1] == 'function') { 88 | // they supplied their own callback 89 | cb = args.pop(); 90 | } 91 | else { 92 | // they didn't supply a callback, so assume they don't care when this 93 | // ends, so, we create our own callback which exits with the number of 94 | // problems when everything is done 95 | cb = function (allResults) { 96 | var problems = 0; 97 | for (var i = 0; i < allResults.length; i++) { 98 | if (allResults[i].status != 'complete' || allResults[i].results.numFailures > 0) { 99 | problems++; 100 | } 101 | } 102 | // we only want to exit after we know everything has been written to 103 | // stdout, otherwise sometimes not all the output from tests will have 104 | // been printed. Thus we write an empty string to stdout and then make sure 105 | // it is 'drained' before exiting 106 | var written = process.stdout.write(''); 107 | if (written) { 108 | process.exit(problems); 109 | } 110 | else { 111 | process.stdout.on('drain', function drained() { 112 | process.stdout.removeListener('drain', drained); 113 | process.exit(problems); 114 | }); 115 | } 116 | } 117 | } 118 | 119 | var fileList = []; 120 | var options = {} 121 | 122 | // fill up options and fileList 123 | exports.parseRunArguments(args, fileList, options, flags); 124 | 125 | // set individual test and suite parallel options 126 | options.testsParallel = options.parallel === true || options.parallel === 'tests' || options.parallel === 'both' ? true : false; 127 | options.suitesParallel = options.parallel === true || options.parallel === 'suites' || options.parallel === 'both' ? true : false; 128 | delete options.parallel; 129 | 130 | if (options.help) { 131 | return generateHelp(flags); 132 | } 133 | 134 | // if we were given no files to run, run the current directory 135 | if (fileList.length === 0) { 136 | fileList = ['.']; 137 | } 138 | 139 | // clean up fileList 140 | for(var i = 0; i < fileList.length; i++) { 141 | // if it is a filename and the filename starts with the current directory 142 | // then remove that so the results are more succinct 143 | if (fileList[i].indexOf(process.cwd()) === 0 && fileList[i].length > (process.cwd().length+1)) { 144 | fileList[i] = fileList[i].replace(process.cwd()+'/', ''); 145 | } 146 | } 147 | 148 | var runner = runners[options.runner || defRunner].module.run; 149 | delete options.runner; 150 | 151 | // if no callback was supplied they don't care about knowing when things 152 | // finish so assume we can exit with the number of 'problems' 153 | if (!cb) { 154 | } 155 | 156 | runner(fileList, options, cb); 157 | } 158 | 159 | exports.parseRunArguments = function(args, fileList, options, flags) { 160 | var arg; 161 | while(arg = args.shift()) { 162 | if (typeof arg == 'string') { 163 | fileList.push(arg); 164 | } 165 | else if(arg.constructor == Array) { 166 | var i = arg == process.ARGV ? 1 : 0; 167 | for (; i < arg.length; i++) { 168 | var found = false; 169 | for (var group in flags) { 170 | for (var j = 0; j < flags[group].length; j++) { 171 | var flag = flags[group][j] 172 | , a = arg[i] 173 | , key = null 174 | , el = null 175 | ; 176 | 177 | if (a.indexOf('=') > -1) { 178 | a = a.split('='); 179 | el = a[1]; 180 | a = a[0]; 181 | } 182 | 183 | if ( (flag.longFlag && a == '--'+flag.longFlag) 184 | || (flag.shortFlag && a == '-'+flag.shortFlag) ) { 185 | key = flag.key || flag.longFlag || flag.shortFlag; 186 | } 187 | 188 | if (key) { 189 | key = dashedToCamelCase(key); 190 | 191 | if (flag.takesValue) { 192 | if (!el) { 193 | if (!flag.options || flag.options.indexOf(arg[i+1]) > -1) { 194 | el = arg.slice(i+1,i+2)[0]; 195 | i++; 196 | } 197 | else { 198 | el = flag.def; 199 | } 200 | } 201 | 202 | if (flag.multiple) { 203 | if (options[key]) { 204 | options[key].push(el); 205 | } 206 | else { 207 | options[key] = [el]; 208 | } 209 | } 210 | else { 211 | options[key] = el; 212 | } 213 | } 214 | else { 215 | options[key] = 'value' in flag ? flag.value : true; 216 | } 217 | break; 218 | } 219 | } 220 | 221 | if (j != flags[group].length) { 222 | found = true; 223 | break; 224 | } 225 | } 226 | 227 | if (!found) { 228 | fileList.push(arg[i]); 229 | } 230 | } 231 | } 232 | else { 233 | for (var key in arg) { 234 | options[key] = arg[key]; 235 | } 236 | } 237 | } 238 | } 239 | 240 | function dashedToCamelCase(key) { 241 | var parts = key.split('-'); 242 | return parts[0] + 243 | parts.slice(1).map(function(str) { return str.substr(0,1).toUpperCase() + str.substr(1); }).join(''); 244 | } 245 | 246 | // creates the help message for running this from the command line 247 | function generateHelp(flags) { 248 | var max = 0; 249 | for (var group in flags) { 250 | for (var i = 0; i < flags[group].length; i++) { 251 | var n = 2; // ' ' 252 | if (flags[group][i].longFlag) { 253 | n += 2; // '--' 254 | n += flags[group][i].longFlag.length; 255 | } 256 | if (flags[group][i].longFlag && flags[group][i].shortFlag) { 257 | n += 2; // ', ' 258 | } 259 | if (flags[group][i].shortFlag) { 260 | n += 1; // '-' 261 | n += flags[group][i].shortFlag.length; 262 | } 263 | 264 | if (n > max) { 265 | max = n; 266 | } 267 | } 268 | } 269 | 270 | console.log('node-async-testing'); 271 | console.log(''); 272 | 273 | for (var group in flags) { 274 | console.log(group+':'); 275 | for (var i = 0; i < flags[group].length; i++) { 276 | var s = ' '; 277 | if (flags[group][i].longFlag) { 278 | s += '--' + flags[group][i].longFlag; 279 | } 280 | if (flags[group][i].longFlag && flags[group][i].shortFlag) { 281 | s += ', '; 282 | } 283 | if (flags[group][i].shortFlag) { 284 | for(var j = s.length+flags[group][i].shortFlag.length; j < max; j++) { 285 | s += ' '; 286 | } 287 | s += '-' + flags[group][i].shortFlag; 288 | } 289 | if (flags[group][i].takesValue) { 290 | s += ' <'+flags[group][i].takesValue+'>'; 291 | } 292 | console.log( 293 | s + 294 | ': ' + 295 | flags[group][i].description + 296 | (flags[group][i].options ? ' ('+flags[group][i].options.join(', ')+')' : '') 297 | ); 298 | } 299 | console.log(''); 300 | } 301 | console.log('Any other arguments are interpreted as files to run'); 302 | } 303 | -------------------------------------------------------------------------------- /lib/testing.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , path = require('path') 3 | , fs = require('fs') 4 | , spawn = require('child_process').spawn 5 | , inspect = require('util').inspect 6 | ; 7 | 8 | /* Runs an object with tests. Each property in the object should be a 9 | * test. A test is just a method. 10 | * 11 | * Available configuration options: 12 | * 13 | * + parallel: boolean, for whether or not the tests should be run in parallel 14 | * or serially. Obviously, parallel is faster, but it doesn't give 15 | * as accurate error reporting 16 | * + testName: string or array of strings, the name of a test to be ran 17 | * + name: string, the name of the suite being ran 18 | * 19 | * Plus, there are options for the following events. These should be functions. 20 | * See docs/running-tests.html for a description of these events. 21 | * 22 | * + onTestStart 23 | * + onTestDone 24 | * + onSuiteDone 25 | */ 26 | exports.runSuite = function(obj, options) { 27 | // make sure options exists 28 | options = options || {}; 29 | 30 | // keep track of internal state 31 | var suite = 32 | { todo: exports.getTestsFromObject(obj, options.testName) 33 | , started: [] 34 | , results: [] 35 | } 36 | 37 | if (suite.todo.length < 1) { return suiteFinished(); } 38 | 39 | process.on('uncaughtException', errorHandler); 40 | process.on('exit', exitHandler); 41 | 42 | // start the test chain 43 | startNextTest(); 44 | 45 | /******** functions ********/ 46 | 47 | function startNextTest() { 48 | var test = suite.todo.shift(); 49 | 50 | if (!test) { return; } 51 | 52 | suite.started.push(test); 53 | 54 | // make sure all tests are an array of test functions 55 | test.func = Array.isArray(test.func) ? test.func : [test.func]; 56 | // TODO make sure test length is odd? 57 | 58 | // keep track of which parts of the flow have been run 59 | test.history = []; 60 | // keep track of assertions made: 61 | test.numAssertions = 0; 62 | // object that is passed to the tests: 63 | test.obj = 64 | { get uncaughtExceptionHandler() { return test.UEHandler; } 65 | , set uncaughtExceptionHandler(h) { 66 | if (options.parallel) { 67 | test.obj.equal('serial', 'parallel', 68 | "Cannot set an 'uncaughtExceptionHandler' when running tests in parallel"); 69 | } 70 | test.UEHandler = h; 71 | } 72 | , finish: function() { testProgressed(test); } 73 | }; 74 | 75 | addAssertionFunctions(test); 76 | 77 | if (options.onTestStart) { options.onTestStart(test.name); } 78 | 79 | runTestFunc(test); 80 | 81 | // if we are supposed to run the tests in parallel, start the next test 82 | if (options.parallel) { 83 | process.nextTick(function() { 84 | startNextTest(); 85 | }); 86 | } 87 | } 88 | 89 | function runTestFunc(test) { 90 | try { 91 | var index = test.history.length; 92 | // mark that we ran the next step 93 | test.history.push(true); 94 | // run the first function 95 | test.func[index](test.obj, test.obj.finish); 96 | } 97 | catch(err) { 98 | errorHandler(err, test); 99 | } 100 | } 101 | 102 | // used when tests are ran, adds the assertion functions to a test object, but 103 | // binds them to that particular test so assertions are properly associated with 104 | // the right test. 105 | function addAssertionFunctions(test) { 106 | for (var funcName in assertionFunctions) { 107 | (function() { 108 | var fn = funcName; 109 | test.obj[fn] = function() { 110 | // if the test doesn't have a func, it was already finished 111 | if (!test.func) { 112 | testAlreadyFinished(test, 'Encountered ' + fn + ' assertion'); 113 | } 114 | try { 115 | assertionFunctions[fn].apply(null, arguments); 116 | test.numAssertions++; 117 | } 118 | catch(err) { 119 | if (err instanceof assert.AssertionError) { 120 | err.TEST = test; 121 | } 122 | throw err; 123 | } 124 | } 125 | })(); 126 | }; 127 | } 128 | 129 | function testAlreadyFinished(test, msg) { 130 | errorHandler(new TestAlreadyFinishedError(test.name + ' already finished!' + (msg ? ' ' + msg : '')), test); 131 | } 132 | 133 | // this is called after each step in a test (each function in the array). 134 | // it figures out what the next step should be and does it 135 | // 136 | // steps come in pairs (except for one middle function), where if the first 137 | // member in a pair is ran then we need to run the second member, 138 | // 139 | // so if the pairs are for setup/teardown (which is what this feature is 140 | // writen for) if a setup is ran, then its corresponding teardown is ran 141 | // regardless of whether the test passes. So, if you start a server or 142 | // something, you can be sure it is stopped. 143 | function testProgressed(test, problem) { 144 | // only keep the first failure that arises in a test 145 | if (!test.failure && problem) { 146 | test.failure = problem; 147 | } 148 | 149 | if (test.func.length == test.history.length) { 150 | testFinished(test); 151 | } 152 | else { 153 | var index = test.history.length; 154 | var match = test.func.length - index - 1; 155 | 156 | if (match >= index) { 157 | // we are still drilling down into the flow, not doing teardowns 158 | if (test.failure) { 159 | // we had a failure, so we don't start any new functions 160 | test.history.push(false); 161 | testProgressed(test); 162 | } 163 | else { 164 | // no failure yet, start next step 165 | runTestFunc(test); 166 | } 167 | } 168 | else { 169 | // we are doing teardowns. We always run a teardown function if its 170 | // matching setup was run 171 | if (test.history[match]) { 172 | // according to the history we ran the match 173 | runTestFunc(test); 174 | } 175 | else { 176 | // didn't run the match so don't run this 177 | test.history.push(false); 178 | testProgressed(test); 179 | } 180 | } 181 | } 182 | } 183 | 184 | function testFinished(test, problem) { 185 | if (!test.failure 186 | && test.obj.numAssertions 187 | && test.obj.numAssertions != test.numAssertions) { 188 | // if they specified the number of assertions, make sure they match up 189 | test.failure = new assert.AssertionError( 190 | { message: 'Wrong number of assertions: ' + test.numAssertions + 191 | ' != ' + test.obj.numAssertions 192 | , actual: test.numAssertions 193 | , expected: test.obj.numAssertions 194 | }); 195 | } 196 | 197 | if (test.failure) { 198 | test.failureType = test.failure instanceof assert.AssertionError ? 'assertion' : 'error'; 199 | delete test.numAssertions; 200 | } 201 | 202 | // remove it from the list of tests that have been started 203 | suite.started.splice(suite.started.indexOf(test), 1); 204 | 205 | test.obj.finish = function() { 206 | testAlreadyFinished(test); 207 | } 208 | // clean up properties that are no longer needed 209 | delete test.obj; 210 | delete test.history; 211 | delete test.func; 212 | 213 | suite.results.push(test); 214 | 215 | if (options.onTestDone) { options.onTestDone(test.failure ? 'failure' : 'success', test); } 216 | 217 | process.nextTick(function() { 218 | // if we have no more tests to start and none still running, we're done 219 | if (suite.todo.length == 0 && suite.started.length == 0) { 220 | suiteFinished(); 221 | } 222 | 223 | startNextTest(); 224 | }); 225 | } 226 | 227 | function errorHandler(err, test) { 228 | // assertions throw an error, but we can't just catch those errors, because 229 | // then the rest of the test will run. So, we don't catch it and it ends up 230 | // here. When that happens just finish the test. 231 | if (err instanceof assert.AssertionError && err.TEST) { 232 | var t = err.TEST; 233 | delete err.TEST; 234 | return testProgressed(t, err); 235 | } 236 | 237 | // if the error is not an instance of Error (& has no stack trace) wrap in proper error 238 | 239 | // console.log(inspect(err)) 240 | if('object' !== typeof err) 241 | err = new Error (typeof err + " thrown:" + inspect(err) + " (intercepted by async_testing)") 242 | 243 | // We want to allow tests to supply a function for handling uncaught errors, 244 | // and since all uncaught errors come here, this is where we have to handle 245 | // them. 246 | // (you can only handle uncaught errors when not in parallel mode) 247 | if (!options.parallel && suite.started.length && suite.started[0].UEHandler) { 248 | // an error could possibly be thrown in the UncaughtExceptionHandler, in 249 | // this case we do not want to call the handler again, so we move it 250 | suite.started[0].UEHandlerUsed = suite.started[0].UEHandler; 251 | delete suite.started[0].UEHandler; 252 | 253 | try { 254 | // run the UncaughtExceptionHandler 255 | suite.started[0].UEHandlerUsed(err); 256 | return; 257 | } 258 | catch(e) { 259 | // we had an error, just run our error handler function on this error 260 | // again. We don't have to worry about it triggering the uncaught 261 | // exception handler again because we moved it just a second ago 262 | return errorHandler(e); 263 | } 264 | } 265 | 266 | if (!(err instanceof TestAlreadyFinishedError) && (test || suite.started.length == 1)) { 267 | // if we can narrow down what caused the error then report it 268 | test = test || suite.started[0]; 269 | testProgressed(test, err); 270 | } 271 | else { 272 | // otherwise report that we can't narrow it down and exit 273 | process.removeListener('uncaughtException', errorHandler); 274 | process.removeListener('exit', exitHandler); 275 | 276 | if (options.onSuiteDone) { 277 | var tests = test ? [test] : suite.started; 278 | if (tests.length < 1) { 279 | tests = suite.results; 280 | } 281 | options.onSuiteDone('error', { error: err, tests: tests.map(function(t) { return t.name; })}); 282 | process.exit(1); 283 | } 284 | else { 285 | // TODO test this 286 | throw err; 287 | } 288 | } 289 | } 290 | 291 | function exitHandler() { 292 | if (suite.started.length > 0) { 293 | if (options.onSuiteDone) { 294 | options.onSuiteDone('exit', {tests: suite.started.map(function(t) { return t.name; })}); 295 | } 296 | } 297 | } 298 | 299 | // clean up method which notifies all listeners of what happened 300 | function suiteFinished() { 301 | if (suite.finished) { return; } 302 | 303 | suite.finished = true; 304 | 305 | process.removeListener('uncaughtException', errorHandler); 306 | process.removeListener('exit', exitHandler); 307 | 308 | if (options.onSuiteDone) { 309 | var result = 310 | { tests: suite.results 311 | , numFailures: 0 312 | , numSuccesses: 0 313 | }; 314 | 315 | 316 | suite.results.forEach(function(r) { 317 | result[r.failure ? 'numFailures' : 'numSuccesses']++; 318 | }); 319 | 320 | options.onSuiteDone('complete', result); 321 | } 322 | } 323 | } 324 | 325 | exports.runFile = function(modulepath, options) { 326 | options = options || {}; 327 | if (options.testName && !Array.isArray(options.testName)) { 328 | options.testName = [options.testName]; 329 | } 330 | 331 | var child = spawn(process.execPath, [ __dirname+'/child.js' 332 | , modulepath 333 | , JSON.stringify(options.parallel || false) 334 | , JSON.stringify(options.testName || null) 335 | ]); 336 | 337 | var buffer = ''; 338 | child.stdout.on('data', function(data) { 339 | data = data.toString(); 340 | 341 | var lines = data.split('\n'); 342 | 343 | lines[0] = buffer + lines[0]; 344 | buffer = lines.pop(); 345 | 346 | lines = exports.messageDecode(lines); 347 | 348 | for (var i = 0; i < lines.length; i++) { 349 | if (typeof lines[i] === 'string') { 350 | console.log(lines[i]); 351 | } 352 | else if (options[lines[i][0]]) { 353 | options[lines[i][0]].apply(null, lines[i].slice(1)); 354 | } 355 | } 356 | }); 357 | 358 | var errorBuffer = ''; 359 | child.stderr.on('data', function(data) { 360 | errorBuffer += data.toString(); 361 | }); 362 | 363 | child.stderr.on('close', function() { 364 | if (errorBuffer && options.onSuiteDone) { 365 | options.onSuiteDone('loadError', {stderr: errorBuffer.trim()}); 366 | } 367 | }); 368 | 369 | return child; 370 | } 371 | 372 | // expandFiles takes a file name, directory name or an array composed of any 373 | // combination of either. 374 | // It recursively searches through directories for test files. 375 | // It gets an absolute path for each file (original file names might be relative) 376 | // returns an array of file objects which look like: 377 | // { name: 'the passed in path' 378 | // , path: 'the absolute path to the file with the extension removed (for requiring)' 379 | // , index: index in the array, later on this is useful for when passing around 380 | // suites 381 | // } 382 | exports.expandFiles = function(list, suiteNames, cb) { 383 | if (typeof cb === 'undefined') { 384 | cb = suiteNames; 385 | suiteNames = null; 386 | } 387 | 388 | if (!Array.isArray(list)) { 389 | list = [list]; 390 | } 391 | if (suiteNames && !Array.isArray(suiteNames)) { 392 | suiteNames = [suiteNames]; 393 | } 394 | if (suiteNames && suiteNames.length === 0) { 395 | suiteNames = null; 396 | } 397 | 398 | var suites = [] 399 | , explicit = [] 400 | ; 401 | 402 | for(var i = 0; i < list.length; i++) { 403 | explicit.push(list[i]); 404 | } 405 | 406 | processNextItem(); 407 | 408 | function foundSuite(suite) { 409 | if (suites.some(function(el) { return el.path == suite.path })) { 410 | // we've already added this file 411 | return; 412 | } 413 | if (suiteNames && suiteNames.indexOf(suite.name) < 0) { 414 | // this file doesn't match the specified suiteNames 415 | return; 416 | } 417 | 418 | suite.index = suites.length; 419 | suites.push(suite); 420 | } 421 | 422 | function processNextItem() { 423 | if( list.length == 0 ) { 424 | return cb(null, suites); 425 | } 426 | 427 | var item = list.shift(); 428 | 429 | // must be a filename 430 | var file = item; 431 | if (file.charAt(0) !== '/') { 432 | file = path.join(process.cwd(),file); 433 | } 434 | fs.stat(file, function(err, stat) { 435 | if (err) { 436 | if (err.errno == 2) { 437 | console.log('No such file or directory: '+file); 438 | console.log(''); 439 | processNextItem(); 440 | return; 441 | } 442 | else { 443 | throw err; 444 | } 445 | } 446 | 447 | if (stat.isFile()) { 448 | // if they explicitly requested this file make sure to grab it 449 | // regardless of its name, otherwise when recursing into directories 450 | // only grab files that start with "test-" and end with ".js" 451 | if (explicit.indexOf(item) >= 0 || path.basename(file).match(/^test-.*\.js$/)) { 452 | var p = path.join(path.dirname(file), path.basename(file, path.extname(file))); 453 | foundSuite({name: item, path: p}); 454 | } 455 | processNextItem(); 456 | } 457 | else if (stat.isDirectory()) { 458 | fs.readdir(file, function(err, files) { 459 | if (err) { 460 | cb(err); 461 | } 462 | for(var i = 0; i < files.length; i++) { 463 | // don't look at hidden files of directores 464 | if (files[i].match(/^[^.]/)) { 465 | list.push(path.join(item,files[i])); 466 | } 467 | } 468 | 469 | processNextItem(); 470 | }); 471 | } 472 | }); 473 | } 474 | } 475 | 476 | // store the assertion functions available to tests 477 | var assertionFunctions = {}; 478 | 479 | // this allows people to add custom assertion functions. 480 | // 481 | // An assertion function needs to throw an error that is `instanceof 482 | // assert.AssertionError` so it is possible to distinguish between runtime 483 | // errors and failures. I recommend the `assert.fail` method. 484 | exports.registerAssertion = function(name, func) { 485 | assertionFunctions[name] = func; 486 | } 487 | 488 | // register the default functions 489 | var assertionModuleAssertions = [ 'ok', 'equal', 'notEqual', 'deepEqual', 'notDeepEqual', 'strictEqual', 'notStrictEqual', 'throws', 'doesNotThrow', 'ifError']; 490 | assertionModuleAssertions.forEach(function(funcName) { 491 | exports.registerAssertion(funcName, assert[funcName]); 492 | }); 493 | 494 | // this is a recursive function because suites can hold sub suites 495 | exports.getTestsFromObject = function(o, filter, namespace) { 496 | var tests = []; 497 | for(var key in o) { 498 | var displayName = (namespace ? namespace+' \u2192 ' : '') + key; 499 | if (typeof o[key] == 'function' || Array.isArray(o[key])) { 500 | // if the testName option is set, then only add the test to the todo 501 | // list if the name matches 502 | if (!filter || filter.indexOf(key) >= 0) { 503 | tests.push({name: displayName , func: o[key]}); 504 | } 505 | } 506 | else { 507 | tests = tests.concat(exports.getTestsFromObject(o[key], filter, displayName)); 508 | } 509 | } 510 | 511 | return tests; 512 | } 513 | 514 | var messageFrame = "~m~"; 515 | // these encode/decode functions inspired by socket.io's 516 | exports.messageDecode = function(lines) { 517 | return lines.map(function(str) { 518 | if (str.substr(0,3) !== messageFrame) { 519 | return str; 520 | } 521 | 522 | var msg = []; 523 | for (var i = 3, number = '', l = str.length; i < l; i++){ 524 | var n = Number(str.substr(i, 1)); 525 | if (str.substr(i, 1) == n){ 526 | number += n; 527 | } else { 528 | number = Number(number); 529 | var m = str.substr(i+messageFrame.length, number); 530 | msg.push(JSON.parse(m)); 531 | i += messageFrame.length*2 + number - 1; 532 | number = ''; 533 | } 534 | } 535 | return msg; 536 | }); 537 | } 538 | exports.messageEncode = function() { 539 | var r = ''; 540 | 541 | for (var i = 0; i < arguments.length; i++) { 542 | var json = JSON.stringify(arguments[i]); 543 | r += messageFrame + json.length + messageFrame + json; 544 | } 545 | 546 | return r; 547 | } 548 | 549 | var TestAlreadyFinishedError = function(message) { 550 | this.name = "TestAlreadyFinishedError"; 551 | this.message = message; 552 | Error.captureStackTrace(this); 553 | }; 554 | TestAlreadyFinishedError.__proto__ = Error.prototype; 555 | 556 | -------------------------------------------------------------------------------- /lib/web-runner.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | , path = require('path') 3 | , fs = require('fs') 4 | , testing = require('./testing') 5 | , spawn = require('child_process').spawn 6 | , io 7 | ; 8 | 9 | try { 10 | var util = require('util'); 11 | } 12 | catch(err) { 13 | if( err.message == "Cannot find module 'util'" ) { 14 | var util = require('sys'); 15 | } 16 | } 17 | 18 | exports.name = 'Web'; 19 | 20 | exports.runnerFlag = 21 | { description: 'use the web runner for running tests from your browser' 22 | , longFlag: 'web' 23 | , key: 'runner' 24 | , value: 'Web' 25 | , shortFlag: 'w' 26 | }; 27 | 28 | exports.optionsFlags = 29 | [ { longFlag: 'port' 30 | , description: 'which port to run the web server on' 31 | , varName: 'port' 32 | } 33 | ]; 34 | 35 | exports.run = function(list, options) { 36 | try { 37 | io = require('socket.io') 38 | } 39 | catch(err) { 40 | if (err.message == 'Cannot find module \'socket.io\'') { 41 | console.log('Dependency socket.io is not installed. Install it with:'); 42 | console.log(''); 43 | console.log(' npm install socket.io'); 44 | } 45 | console.log(''); 46 | console.log('node-async-testing does not depend on it to run; it is only used'); 47 | console.log('by the web runner. Thus it isn\'t listed as dependency through npm.'); 48 | return; 49 | } 50 | 51 | options.port = parseInt(options.port); 52 | if (isNaN(options.port)) { 53 | options.port = 8765; 54 | } 55 | 56 | var suites 57 | , queue = [] 58 | , running = [] 59 | , messageQueue = [] 60 | , socket 61 | ; 62 | 63 | testing.expandFiles(list, options.suiteName, startServer); 64 | 65 | function startServer(err, loaded) { 66 | if (err) { 67 | throw err; 68 | } 69 | 70 | suites = loaded; 71 | 72 | suites.sort(function(a, b) { 73 | if (a < b) { return -1; } 74 | if (a > b) { return 1; } 75 | return 0; 76 | }); 77 | 78 | for (var i = 0; i < suites.length; i++) { 79 | suites[i].index = i; 80 | suites[i].queuedTestResults = []; 81 | } 82 | 83 | for (var i = 0; i < suites.length; i++) { 84 | suites[i].parallel = options.testsParallel; 85 | } 86 | 87 | var dir = __dirname + '/web-runner/public'; 88 | 89 | server = http.createServer(function(request, response){ 90 | var filename = request.url; 91 | if (request.url == '/') { 92 | filename = '/index.html'; 93 | } 94 | 95 | //console.log('request for '+filename); 96 | 97 | path.exists(dir+filename, function(exists) { 98 | if (exists) { 99 | response.writeHead(200, {'content-type': contenttypes[path.extname(filename)]}); 100 | util.pump(fs.createReadStream(dir+filename), response); 101 | } 102 | else { 103 | console.log('cannot find file: ' + filename); 104 | response.writeHead(404, {'content-type': 'text/plain'}); 105 | response.write('Not Found: ' + filename); 106 | response.end(); 107 | } 108 | }); 109 | }); 110 | 111 | loadFiles(suites, function() { 112 | server.listen(options.port); 113 | 114 | // socket.io, I choose you 115 | socket = io.listen(server, {log: function() {}}); 116 | 117 | socket.on('connection', function(client) { 118 | // connected!! 119 | 120 | client.on('message', function(msg) { 121 | obj = JSON.parse(msg); 122 | 123 | if (obj.cmd in socketHandlers) { 124 | socketHandlers[obj.cmd](obj, client); 125 | } 126 | else { 127 | console.log('unknown socket.io message:'); 128 | console.log(obj); 129 | } 130 | }); 131 | 132 | client.send(JSON.stringify({cmd: 'suitesList', suites: suites})); 133 | 134 | // send the current state 135 | for (var i = 0; i < queue.length; i++) { 136 | client.send(JSON.stringify({cmd: 'queued', suite: queue[i][0], parallel: queue[i][1]})); 137 | } 138 | for (var i = 0; i < running.length; i++) { 139 | client.send(JSON.stringify({cmd: 'suiteStart', suite: running[i].index})); 140 | for (var i = 0; i < running[i].testsStarted.length; i++) { 141 | client.send(JSON.stringify({cmd: 'testStart', name: running[i].testsStarted[i]})); 142 | } 143 | for (var i = 0; i < running[i].testsDone.length; i++) { 144 | client.send(JSON.stringify({cmd: 'testDone', result: running[i].testsDone[i]})); 145 | } 146 | } 147 | }); 148 | 149 | console.log('Web runner started\nhttp://localhost:'+options.port+'/'); 150 | 151 | suites.forEach(watchFile) 152 | }); 153 | } 154 | 155 | function watchFile(suite) { 156 | fs.watchFile(suite.name, {interval: 500}, watchFunction) 157 | 158 | function watchFunction(o, n) { 159 | if (n.mtime.toString() === o.mtime.toString()) { 160 | return; 161 | } 162 | loadFiles(suite, fileLoaded); 163 | } 164 | 165 | function fileLoaded() { 166 | messageQueue.push(JSON.stringify({cmd: 'suitesList', suites: [suite]})); 167 | checkQueue(); 168 | } 169 | } 170 | 171 | function checkQueue() { 172 | if (running.length && !options.suitesParallel) { 173 | // already running a test 174 | return; 175 | } 176 | 177 | var cmd = queue.shift(); 178 | 179 | if (!cmd) { 180 | // no tests scheduled 181 | while (messageQueue.length > 0) { 182 | socket.broadcast(messageQueue.shift()); 183 | } 184 | return; 185 | } 186 | 187 | var suite = suites[cmd[0]]; 188 | 189 | var opts = 190 | { parallel: cmd[1] 191 | , testName: options.testName 192 | , onTestStart: function(name) { 193 | workerHandlers.onTestStart(suite, name); 194 | } 195 | , onTestDone: function(status, result) { 196 | workerHandlers.onTestDone(suite, status, result); 197 | } 198 | , onSuiteDone: function(status, results) { 199 | workerHandlers['onSuite'+status.substr(0,1).toUpperCase()+status.substr(1)](suite, results); 200 | } 201 | } 202 | 203 | suite.testsStarted = []; 204 | suite.testsDone = []; 205 | 206 | var msg = {cmd: 'suiteStart', suite: suite.index}; 207 | socket.broadcast(JSON.stringify(msg)); 208 | 209 | running.push(suite); 210 | suite.startTime = new Date(); 211 | suite.child = testing.runFile(suite.path, opts); 212 | } 213 | 214 | function loadFiles(files, cb) { 215 | if (files.constructor != Array) { 216 | files = [files]; 217 | } 218 | 219 | var index = 0; 220 | processNextItem(); 221 | 222 | function processNextItem() { 223 | if (index >= files.length) { 224 | return cb(); 225 | } 226 | 227 | var scopedIndex = index; 228 | var child = spawn(process.execPath, [ __dirname+'/web-runner/child-loader.js' 229 | , files[index].path 230 | ]); 231 | var buffer = ''; 232 | var errorBuffer = ''; 233 | 234 | index++; 235 | 236 | child.stdout.on('data', function(data) { 237 | data = data.toString(); 238 | 239 | var lines = data.split('\n'); 240 | 241 | lines[0] = buffer + lines[0]; 242 | buffer = lines.pop(); 243 | 244 | lines = testing.messageDecode(lines); 245 | 246 | for (var i = 0; i < lines.length; i++) { 247 | if (typeof lines[i] === 'string') { 248 | console.log(lines[i]); 249 | } 250 | else { 251 | delete files[scopedIndex].error; 252 | files[scopedIndex].tests = lines[i][0]; 253 | } 254 | } 255 | }); 256 | 257 | 258 | child.on('exit', function(code) { 259 | if (code !== 0) { 260 | delete files[scopedIndex].tests; 261 | files[scopedIndex].error = { message: 'Cannot load file' }; 262 | } 263 | processNextItem(); 264 | }); 265 | } 266 | } 267 | 268 | function cleanupSuite(suite) { 269 | // todo remove from running array 270 | delete suite.testsStarted; 271 | delete suite.testsDone; 272 | delete suite.child; 273 | 274 | running.splice(running.indexOf(suite)); 275 | 276 | checkQueue(); 277 | } 278 | 279 | var socketHandlers = 280 | { enqueueSuite: function(obj, client) { 281 | for (var i = 0; i < queue.length; i++) { 282 | if (queue[i][0] == obj.index) { 283 | // don't add a suite to the queue, that is already in it 284 | return; 285 | } 286 | } 287 | queue.push([obj.index, obj.parallel]); 288 | var msg = {cmd: 'queued', suite: obj.index, parallel: obj.parallel}; 289 | client.broadcast(JSON.stringify(msg)); 290 | checkQueue(); 291 | } 292 | , cancel: function(obj, client) { 293 | var suite = suites[obj.index]; 294 | if (suite.child) { 295 | suite.child.kill(); 296 | socket.broadcast(JSON.stringify({cmd: 'cancelled', suite: suite.index})); 297 | cleanupSuite(suite); 298 | } 299 | } 300 | } 301 | 302 | var workerHandlers = 303 | { onTestStart: function(suite, name) { 304 | suite.testsStarted.push(name); 305 | var msg = {cmd: 'testStart', suite: suite.index, name: name}; 306 | socket.broadcast(JSON.stringify(msg)); 307 | } 308 | , onTestDone: function(suite, status, result) { 309 | suite.testsDone.push(result); 310 | 311 | var msg = {cmd: 'testDone', suite: suite.index, result: result}; 312 | socket.broadcast(JSON.stringify(msg)); 313 | } 314 | , onSuiteComplete: function(suite, results) { 315 | var msg = {cmd: 'suiteDone', suite: suite.index, numSuccesses: results.numSuccesses, numFailures: results.numFailures}; 316 | socket.broadcast(JSON.stringify(msg)); 317 | cleanupSuite(suite); 318 | } 319 | , onSuiteLoadError: function(suite, obj) { 320 | var msg = {cmd: 'suiteLoadError', suite: suite.index}; 321 | socket.broadcast(JSON.stringify(msg)); 322 | cleanupSuite(suite); 323 | } 324 | , onSuiteError: function(suite, results) { 325 | var msg = {cmd: 'suiteError', suite: suite.index, error: results.error, tests: results.tests}; 326 | socket.broadcast(JSON.stringify(msg)); 327 | cleanupSuite(suite); 328 | } 329 | , onSuiteExit: function(suite, results) { 330 | var msg = {cmd: 'suiteExit', suite: suite.index, tests: results.tests}; 331 | socket.broadcast(JSON.stringify(msg)); 332 | cleanupSuite(suite); 333 | } 334 | } 335 | } 336 | 337 | var contenttypes = 338 | { '.html': 'text/html' 339 | , '.css': 'text/css' 340 | , '.js': 'application/javascript' 341 | } 342 | 343 | -------------------------------------------------------------------------------- /lib/web-runner/child-loader.js: -------------------------------------------------------------------------------- 1 | var testing = require('../testing') 2 | , suite = require(process.ARGV[2]) 3 | , tests = testing.getTestsFromObject(suite) 4 | , msg = {} 5 | ; 6 | 7 | for (var i = 0; i < tests.length; i++) { 8 | msg[tests[i].name] = { func: ''+tests[i].func }; 9 | } 10 | 11 | postMessage(msg); 12 | 13 | function postMessage(str) { 14 | console.log(testing.messageEncode(str)); 15 | } 16 | -------------------------------------------------------------------------------- /lib/web-runner/public/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bentomas/node-async-testing/82384ba4b8444e4659464bad661788827ea721aa/lib/web-runner/public/error.png -------------------------------------------------------------------------------- /lib/web-runner/public/failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bentomas/node-async-testing/82384ba4b8444e4659464bad661788827ea721aa/lib/web-runner/public/failure.png -------------------------------------------------------------------------------- /lib/web-runner/public/images.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 27 | 28 | 46 | 48 | 49 | 51 | image/svg+xml 52 | 54 | 55 | 56 | 57 | 58 | 63 | 71 | 74 | 85 | 96 | 97 | 100 | 111 | 122 | 123 | 126 | 139 | 152 | 153 | 158 | 164 | 177 | 190 | 191 | 197 | 210 | 223 | 224 | 230 | 243 | 256 | 257 | 263 | 276 | 289 | 290 | 291 | 292 | 293 | -------------------------------------------------------------------------------- /lib/web-runner/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Node Async Testing 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/web-runner/public/main.js: -------------------------------------------------------------------------------- 1 | io.setPath('/client/'); 2 | 3 | (function() { 4 | var container, suitesList, files, suites; 5 | 6 | var socket = new io.Socket(null, {transports: ['websocket', 'htmlfile', 'xhr-multipart', 'xhr-polling']}); 7 | socket.connect(); 8 | 9 | socket.on('connect', function() { 10 | // create doc 11 | container = document.createElement('DIV'); 12 | container.id = 'container'; 13 | 14 | var suitesHeader = document.createElement('H2'); 15 | suitesHeader.innerHTML = 'Suites'; 16 | suitesList = document.createElement('UL'); 17 | suitesList.id = 'suites-list'; 18 | 19 | var runAllSpan = document.createElement('SPAN'); 20 | runAllSpan.className = 'run-all-button'; 21 | runAllSpan.innerHTML = 'Run All'; 22 | runAllSpan.onclick = function(e) { 23 | suites.forEach(enqueueSuite); 24 | } 25 | suitesHeader.appendChild(runAllSpan); 26 | 27 | container.appendChild(suitesHeader); 28 | container.appendChild(suitesList); 29 | 30 | document.body.appendChild(container); 31 | }); 32 | 33 | socket.on('message', function(msg) { 34 | obj = JSON.parse(msg); 35 | 36 | if (obj.cmd in handlers) { 37 | handlers[obj.cmd](obj); 38 | } 39 | else { 40 | console.log(obj); 41 | } 42 | }); 43 | 44 | var handlers = 45 | { queued: function(obj) { 46 | suite = suites[obj.suite]; 47 | suite.parallel = obj.parallel; 48 | suiteQueued(suite); 49 | } 50 | , suitesList: function(obj) { 51 | suites = suites || []; 52 | obj.suites.forEach(function(s) { 53 | if (s.index+1 > suites.length) { 54 | suites.push(s); 55 | suitesList.appendChild(buildSuiteElement(s)); 56 | } 57 | else { 58 | clearSuiteResults(suites[s.index]); 59 | suites[s.index] = s; 60 | var old = suitesList.childNodes[s.index]; 61 | suitesList.insertBefore(buildSuiteElement(s), old); 62 | suitesList.removeChild(old); 63 | if (/(^|\s)open(\s|$)/.test(old.className)) { 64 | s.el.className += ' open'; 65 | } 66 | } 67 | }); 68 | } 69 | , suiteStart: function(obj) { 70 | suite = suites[obj.suite]; 71 | 72 | var el = suite.el; 73 | 74 | removeClass(el, 'queued'); 75 | el.className += ' running'; 76 | 77 | suite.runSpan.innerHTML = 'Running ['; 78 | var cancelSpan = document.createElement('SPAN'); 79 | cancelSpan.className = 'cancel'; 80 | cancelSpan.innerHTML = 'x'; 81 | cancelSpan.onclick = function cancel() { 82 | socket.send(JSON.stringify({cmd: 'cancel', index: suite.index})); 83 | } 84 | suite.runSpan.appendChild(cancelSpan); 85 | suite.runSpan.appendChild(document.createTextNode(']')); 86 | } 87 | , testStart: function(obj) { 88 | suite = suites[obj.suite]; 89 | 90 | var test = suite.tests[obj.name]; 91 | test.el.className += ' running'; 92 | } 93 | , testDone: function(obj) { 94 | suite = suites[obj.suite]; 95 | 96 | // normal error 97 | var test = suite.tests[obj.result.name]; 98 | var el = test.el; 99 | 100 | removeClass(el, 'running'); 101 | if (obj.result.failure) { 102 | if (obj.result.failureType == 'assertion') { 103 | suite.numFailures++; 104 | el.className += ' failure'; 105 | } 106 | else { 107 | suite.numErrors++; 108 | el.className += ' error'; 109 | } 110 | 111 | var code = document.createElement('CODE'); 112 | code.className = 'result'; 113 | code.innerHTML = (obj.result.failure).stack; 114 | el.insertBefore(code, el.lastChild); 115 | 116 | var summarySpan = document.createElement('SPAN'); 117 | summarySpan.className = 'summary'; 118 | summarySpan.innerHTML = (obj.result.failure).message; 119 | test.nameEl.appendChild(summarySpan); 120 | } 121 | else { 122 | el.className += ' success'; 123 | } 124 | } 125 | , suiteDone: function(obj) { 126 | suite = suites[obj.suite]; 127 | 128 | var el = suite.el; 129 | 130 | if (suite.numErrors > 0) { 131 | var status = 'error'; 132 | } 133 | else if (suite.numFailures > 0) { 134 | var status = 'failure'; 135 | } 136 | else { 137 | var status = 'success'; 138 | } 139 | 140 | updateFavicon(status); 141 | 142 | removeClass(el, 'running'); 143 | el.className += ' '+status; 144 | 145 | var input = el.getElementsByTagName('input')[0]; 146 | input.disabled = false; 147 | 148 | var doneSpan = document.createElement('SPAN'); 149 | doneSpan.className = 'done'; 150 | doneSpan.innerHTML = 'Done'; 151 | el.insertBefore(doneSpan, suite.runSpan); 152 | 153 | doneSpan.onclick = function(e) { 154 | clearSuiteResults(suite); 155 | } 156 | 157 | suite.runSpan.innerHTML = 'Run'; 158 | } 159 | , suiteError: function(obj) { 160 | suite = suites[obj.suite]; 161 | 162 | var el = suite.el; 163 | 164 | if (obj.tests.length > 1) { 165 | for (var i = 0; i < obj.tests.length; i++) { 166 | var test = suite.tests[obj.tests[i]]; 167 | removeClass(test.el, 'running'); 168 | test.el.className += ' error'; 169 | 170 | var code = document.createElement('P'); 171 | code.className = 'result'; 172 | code.innerHTML = 'The specific error for this test could not be determined. See the \'Non-specific Error\' below.'; 173 | test.el.insertBefore(code, test.el.lastChild); 174 | 175 | var summarySpan = document.createElement('SPAN'); 176 | summarySpan.className = 'summary'; 177 | summarySpan.innerHTML = 'Non-specific error'; 178 | test.nameEl.appendChild(summarySpan); 179 | } 180 | 181 | var li = document.createElement('LI'); 182 | li.className = 'non-specific-errors'; 183 | 184 | var span = document.createElement('SPAN'); 185 | span.className = 'name'; 186 | span.innerHTML = 'Non-specific Error'; 187 | 188 | span.onclick = function(e) { 189 | toggleItem({el: li}); 190 | } 191 | li.appendChild(span); 192 | 193 | code = document.createElement('CODE'); 194 | code.className = 'result'; 195 | code.appendChild(document.createTextNode(obj.error.stack)); 196 | li.appendChild(code); 197 | 198 | el.getElementsByClassName('tests-list')[0].appendChild(li); 199 | } 200 | else { 201 | var test = suite.tests[obj.tests[0]]; 202 | 203 | removeClass(test.el, 'running'); 204 | test.el.className += ' error'; 205 | 206 | var code = document.createElement('CODE'); 207 | code.className = 'result'; 208 | code.innerHTML = obj.error.stack; 209 | test.el.insertBefore(code, test.el.lastChild); 210 | 211 | var summarySpan = document.createElement('SPAN'); 212 | summarySpan.className = 'summary'; 213 | summarySpan.innerHTML = obj.error.message; 214 | test.nameEl.appendChild(summarySpan); 215 | } 216 | 217 | var status = 'error'; 218 | updateFavicon(status); 219 | 220 | removeClass(el, 'running'); 221 | el.className += ' '+status; 222 | 223 | var input = el.getElementsByTagName('input')[0]; 224 | input.disabled = false; 225 | 226 | var doneSpan = document.createElement('SPAN'); 227 | doneSpan.className = 'done'; 228 | doneSpan.innerHTML = 'Done'; 229 | el.insertBefore(doneSpan, suite.runSpan); 230 | 231 | doneSpan.onclick = function(e) { 232 | clearSuiteResults(suite); 233 | } 234 | 235 | suite.runSpan.innerHTML = 'Run'; 236 | } 237 | , suiteLoadError: function(obj) { 238 | var suite = suites[obj.suite]; 239 | 240 | removeClass(suite.el, 'running'); 241 | suite.el.className += ' compile-error'; 242 | suite.runSpan.innerHTML = 'Error running'; 243 | suite.runSpan.onclick = null; 244 | 245 | clearSuiteResults(suite); 246 | } 247 | , cancelled: function(obj) { 248 | var suite = suites[obj.suite]; 249 | var el = suite.el; 250 | 251 | var input = el.getElementsByTagName('input')[0]; 252 | input.disabled = false; 253 | 254 | removeClass(el, 'running'); 255 | el.className += ' '+status; 256 | 257 | clearSuiteResults(suite); 258 | 259 | suite.runSpan.innerHTML = 'Run'; 260 | } 261 | }; 262 | 263 | function clearSuiteResults(suite) { 264 | removeClass(suite.el, ['success', 'failure', 'error']); 265 | 266 | suite.numFailures = 0; 267 | suite.numErrors = 0; 268 | 269 | var els = suite.el.getElementsByClassName('non-specific-errors'); 270 | while(els.length) { 271 | els[0].parentNode.removeChild(els[0]); 272 | } 273 | 274 | var testLis = suite.el.getElementsByTagName('li'); 275 | for (var i=0; i < testLis.length; i++) { 276 | removeClass(testLis[i], ['success', 'failure', 'error', 'running']); 277 | } 278 | 279 | els = suite.el.getElementsByClassName('done'); 280 | while(els.length) { 281 | els[0].parentNode.removeChild(els[0]); 282 | } 283 | 284 | els = suite.el.getElementsByClassName('result'); 285 | while(els.length) { 286 | els[0].parentNode.removeChild(els[0]); 287 | } 288 | 289 | els = suite.el.getElementsByClassName('summary'); 290 | while(els.length) { 291 | els[0].parentNode.removeChild(els[0]); 292 | } 293 | } 294 | 295 | function buildSuiteElement(suite) { 296 | var suiteLi = document.createElement('LI'); 297 | 298 | var nameSPAN = document.createElement('SPAN'); 299 | nameSPAN.className = 'name'; 300 | nameSPAN.innerHTML = suite.name; 301 | nameSPAN.onclick = function(e) { 302 | toggleItem(suite); 303 | } 304 | suiteLi.appendChild(nameSPAN); 305 | 306 | if (suite.error) { 307 | suiteLi.className = 'compile-error'; 308 | 309 | var runSpan = document.createElement('SPAN'); 310 | runSpan.className = 'run-button'; 311 | if (suite.error.message || suite.error.lineno || suite.error.stack) { 312 | runSpan.innerHTML = 'Error: ' + (suite.error.lineno ? 'line ' + suite.error.lineno + ' - ' : '') + suite.error.message; 313 | } 314 | else { 315 | runSpan.innerHTML = 'Error loading file'; 316 | } 317 | suiteLi.appendChild(runSpan); 318 | suite.runSpan = runSpan; 319 | } 320 | else { 321 | var runSpan = document.createElement('SPAN'); 322 | runSpan.className = 'run-button'; 323 | runSpan.innerHTML = 'Run'; 324 | runSpan.onclick = function(e) { 325 | enqueueSuite(suite); 326 | //toggleItem(suite, true); 327 | } 328 | suiteLi.appendChild(runSpan); 329 | suite.runSpan = runSpan; 330 | 331 | var label = document.createElement('LABEL'); 332 | label.innerHTML = ' Run in parallel?'; 333 | suiteLi.appendChild(label); 334 | 335 | var testsList = document.createElement('UL'); 336 | testsList.className = 'tests-list'; 337 | 338 | for (var name in suite.tests) { 339 | (function(n) { 340 | var testLi = document.createElement('LI'); 341 | 342 | var span = document.createElement('SPAN'); 343 | span.className = 'name'; 344 | span.innerHTML = n; 345 | span.onclick = function(e) { 346 | toggleItem(suite.tests[n]); 347 | } 348 | testLi.appendChild(span); 349 | 350 | var code = document.createElement('CODE'); 351 | code.className = 'test-func'; 352 | code.innerHTML = suite.tests[n].func; 353 | testLi.appendChild(code); 354 | 355 | suite.tests[n].el = testLi; 356 | suite.tests[n].nameEl = span; 357 | 358 | testsList.appendChild(testLi); 359 | })(name); 360 | } 361 | 362 | suiteLi.appendChild(testsList); 363 | } 364 | 365 | suite.el = suiteLi; 366 | 367 | return suiteLi; 368 | } 369 | 370 | function toggleItem(item, open) { 371 | var el = item.el; 372 | 373 | if (typeof open == 'undefined') { 374 | if (el.className.indexOf('open') < 0) { 375 | open = true; 376 | } 377 | else { 378 | open = false; 379 | } 380 | } 381 | 382 | removeClass(el, 'open'); 383 | 384 | if (open) { 385 | el.className += ' open'; 386 | } 387 | } 388 | 389 | function enqueueSuite(suite) { 390 | if (suite.el.className.indexOf('compile-error') < 0 && suite.el.className.indexOf('queued') < 0 && suite.el.className.indexOf('running') < 0) { 391 | var input = suite.el.getElementsByTagName('input')[0]; 392 | suite.parallel = input.checked; 393 | socket.send(JSON.stringify({cmd: 'enqueueSuite', index: suite.index, parallel: input.checked})); 394 | suiteQueued(suite); 395 | } 396 | } 397 | function suiteQueued(suite) { 398 | clearSuiteResults(suite); 399 | 400 | suite.el.className += ' queued'; 401 | // don't set the inner text till we get the confirmation from the server 402 | suite.runSpan.innerHTML = 'Queued'; 403 | 404 | var input = suite.el.getElementsByTagName('input')[0]; 405 | input.checked = suite.parallel; 406 | input.disabled = true; 407 | } 408 | 409 | function updateFavicon(status) { 410 | var head = document.getElementsByTagName('head')[0] 411 | , links = head.getElementsByTagName('link') 412 | , link 413 | ; 414 | 415 | for (var i = 0; i < links.length; i++) { 416 | if (links[i].getAttribute('rel') == 'icon') { 417 | link = links[i]; 418 | break; 419 | } 420 | } 421 | 422 | if (link) { 423 | head.removeChild(link); 424 | link = document.createElement('LINK'); 425 | link.rel = 'icon'; 426 | link.href = status + '.png'; 427 | head.appendChild(link); 428 | } 429 | } 430 | 431 | 432 | function removeClass(el, classes) { 433 | if (classes.constructor != Array) { 434 | classes = [classes]; 435 | } 436 | 437 | var r = '(^|\\s)('+classes.join('|')+')(\\s|$)'; 438 | el.className = el.className.replace(new RegExp(r), '$1$3').trim(); 439 | } 440 | })(); 441 | -------------------------------------------------------------------------------- /lib/web-runner/public/running.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bentomas/node-async-testing/82384ba4b8444e4659464bad661788827ea721aa/lib/web-runner/public/running.gif -------------------------------------------------------------------------------- /lib/web-runner/public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #CCC; 3 | color: #333; 4 | font-family: 'Helvetica', 'Arial', sans-serif; 5 | } 6 | 7 | #container { 8 | background: #FFF; 9 | box-shadow: 2px 2px 4px #666666; -moz-box-shadow: 2px 2px 4px #666666; -webkit-box-shadow: 2px 2px 4px #666666; 10 | margin: auto; 11 | margin-top: 50px; 12 | margin-bottom: 50px; 13 | padding: 0; 14 | width: 600px; 15 | } 16 | 17 | h2 { 18 | background: #777; 19 | color: #FFF; 20 | font-size: 18px; 21 | margin: 0; 22 | padding: 8px 30px; 23 | position: relative; 24 | } 25 | 26 | .run-all-button { 27 | background: #CCC; 28 | border-radius: 5px; -moz-border-radius: 5px; -webkit-border-radius: 5px; color: #FFF; 29 | color: #333; 30 | cursor: pointer; 31 | display: block; 32 | font-size: 13px; 33 | font-weight: normal; 34 | padding: 3px 8px; 35 | position: absolute; 36 | right: 30px; 37 | top: 8px; 38 | } 39 | 40 | #suites-list { 41 | list-style-type: none; 42 | margin: 0; 43 | padding: 0; 44 | } 45 | #suites-list > li { 46 | border-top: 1px solid #888; 47 | font-size: 14px; 48 | padding: 15px 30px 15px 30px; 49 | position: relative; 50 | } 51 | #suites-list > .success { background: url(success.png) no-repeat 9px 18px; } 52 | #suites-list > .failure { background: url(failure.png) no-repeat 9px 17px; } 53 | #suites-list > .error { background: url(error.png) no-repeat 9px 17px; } 54 | #suites-list > .running { background: url(running.gif) no-repeat 8px 15px; } 55 | #suites-list > li .name { 56 | cursor: pointer; 57 | font-weight: bold; 58 | } 59 | 60 | #suites-list > li .run-button { 61 | background: #777; 62 | border-radius: 5px; -moz-border-radius: 5px; -webkit-border-radius: 5px; color: #FFF; 63 | cursor: pointer; 64 | display: block; 65 | font-size: 13px; 66 | padding: 3px 8px; 67 | position: absolute; 68 | right: 30px; 69 | top: 15px; 70 | } 71 | #suites-list > .queued .run-button , 72 | #suites-list > .running .run-button { 73 | background: transparent; 74 | color: #333; 75 | cursor: default; 76 | } 77 | #suites-list > .compile-error .run-button { 78 | background: transparent; 79 | color: #F00; 80 | cursor: default; 81 | } 82 | #suites-list > .compile-error .name { 83 | cursor: default; 84 | font-weight: normal; 85 | } 86 | 87 | #suites-list > li label { 88 | display: none; 89 | margin-top: 10px; 90 | } 91 | #suites-list > .open label { 92 | display: block; 93 | } 94 | #suites-list > .running label , 95 | #suites-list > .queued label { 96 | color: #AAA; 97 | } 98 | 99 | .tests-list { 100 | border-top: 1px solid #DDD; 101 | display: none; 102 | list-style-type: none; 103 | margin: 10px 0 -5px; 104 | padding: 9px 0 0; 105 | } 106 | .open .tests-list { 107 | display: block; 108 | } 109 | 110 | .tests-list li { 111 | font-weight: normal; 112 | font-size: 12px; 113 | padding: 5px 5px 5px 25px; 114 | position: relative; 115 | } 116 | .tests-list li:hover { 117 | background-color: #EEE; 118 | } 119 | .tests-list .name { 120 | display: block; 121 | overflow: hidden; 122 | white-space: pre; 123 | width: 100%; 124 | } 125 | .tests-list .success { background: url(success.png) no-repeat 4px 7px; } 126 | .tests-list .failure { background: url(failure.png) no-repeat 4px 7px; } 127 | .tests-list .error { background: url(error.png) no-repeat 4px 7px; } 128 | .tests-list .running { background: url(running.gif) no-repeat 2px 5px; } 129 | 130 | .tests-list .result { 131 | border-top: 1px dotted #CCC; 132 | display: none; 133 | margin: 10px 0 0; 134 | padding: 8px 0 5px; 135 | overflow: auto; 136 | white-space: pre; 137 | } 138 | .tests-list p.result { 139 | padding-top: 10px; 140 | padding-bottom: 4px; 141 | } 142 | .tests-list .open .result { 143 | display: block; 144 | } 145 | 146 | .tests-list .test-func { 147 | border-top: 1px dotted #CCC; 148 | display: none; 149 | margin: 5px 0 2px; 150 | padding: 8px 0; 151 | overflow: auto; 152 | white-space: pre; 153 | } 154 | .tests-list .open .test-func { 155 | display: block; 156 | } 157 | 158 | .tests-list .summary { 159 | color: #BBB; 160 | font-weight: normal; 161 | height: 20px; 162 | margin-left: 15px; 163 | } 164 | 165 | .done { 166 | font-size: 11px; 167 | color: #BBB; 168 | cursor: pointer; 169 | margin-left: 15px; 170 | text-transform: lowercase; 171 | } 172 | 173 | .cancel { 174 | color: red; 175 | cursor: pointer; 176 | } 177 | -------------------------------------------------------------------------------- /lib/web-runner/public/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bentomas/node-async-testing/82384ba4b8444e4659464bad661788827ea721aa/lib/web-runner/public/success.png -------------------------------------------------------------------------------- /lib/wrap.js: -------------------------------------------------------------------------------- 1 | const PRESETUP = 0 2 | , SETUP = 1 3 | , SETUPDONE = 2 4 | , SETUPFAILED = 3 5 | ; 6 | 7 | /* convenience function for wrapping a suite with setup and teardown 8 | * functions. this takes an object which has three properties: 9 | * 10 | * (by the way, I'm looking for a better name for this function) 11 | * 12 | * suite: the test suite object, required 13 | * setup: a function that should be run before the test 14 | * teardown: a function that should be run after the test 15 | */ 16 | exports.wrap = function(obj) { 17 | if (!obj.suite) { 18 | throw new Error('Cannot wrap suite. No suite provided'); 19 | } 20 | 21 | var state = PRESETUP 22 | , pendingTests = [] 23 | , numTests = 0 24 | , suites = [obj.suite] 25 | ; 26 | 27 | 28 | for (var i = 0; i < suites.length; i++) { 29 | for(var key in suites[i]) { 30 | if (typeof suites[i][key] == 'function' || Array.isArray(suites[i][key])) { 31 | numTests++; 32 | suites[i][key] = wrapper(suites[i][key], key); 33 | } 34 | else { 35 | suites.push(suites[i][key]); 36 | } 37 | } 38 | } 39 | 40 | return obj.suite; 41 | 42 | function wrapper(func) { 43 | var n = Array.isArray(func) ? func : [func]; 44 | 45 | n.unshift(obj.setup || empty); 46 | 47 | if (obj.suiteSetup) { 48 | n.unshift(function(t, f) { 49 | switch(state) { 50 | case PRESETUP: // suiteSetup hasn't been ran 51 | state = SETUP; 52 | pendingTests.push([t, f]); 53 | obj.suiteSetup(function setupDone() { 54 | state = SETUPDONE; 55 | for (var i = 0; i < pendingTests.length; i++) { 56 | pendingTests[i][1](); 57 | } 58 | }); 59 | break; 60 | case SETUP: // suiteSetup is running 61 | pendingTests.push([t, f]); 62 | break; 63 | case SETUPDONE: // suiteSetup is done 64 | f(); 65 | break; 66 | case SETUPFAILED: // need to fail the test 67 | fail(t); 68 | break; 69 | } 70 | }); 71 | } 72 | else if (obj.suiteTeardown) { 73 | n.unshift(empty); 74 | } 75 | 76 | n.push(obj.teardown || empty); 77 | 78 | if (obj.suiteSetup || obj.suiteTeardown) { 79 | n.push(function(t, f) { 80 | switch(state) { 81 | case SETUP: 82 | // we still think suiteSetup is still running but some test just finished... oops! 83 | state = SETUPFAILED; 84 | for (var i = 0; i < pendingTests.length; i++) { 85 | fail(pendingTests[i][0]); 86 | } 87 | break; 88 | default: 89 | numTests--; 90 | if (numTests == 0 && obj.suiteTeardown) { 91 | obj.suiteTeardown(f); 92 | } 93 | else { 94 | f(); 95 | } 96 | } 97 | }); 98 | } 99 | 100 | var prevToString = func + ''; 101 | n.toString = function() { 102 | var str = 103 | '' + 104 | (obj.setup ? obj.setup + '\n' : '') + 105 | prevToString + '\n' + 106 | (obj.teardown ? obj.teardown + '' : '') 107 | ; 108 | 109 | return str; 110 | } 111 | 112 | return n; 113 | } 114 | 115 | } 116 | 117 | function fail(test, message) { 118 | test.ok(false, message || 'Suite Setup failed'); 119 | } 120 | 121 | function empty(t, f) { 122 | f(); 123 | } 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name": "async_testing" 2 | , "description": "A simple Node testing library." 3 | , "version": "0.3.2" 4 | , "author": "Benjamin Thomas" 5 | , "main": "./lib/async_testing" 6 | , "directories": {"lib":"./lib"} 7 | , "repository": 8 | { "type": "git" 9 | , "url": "http://github.com/bentomas/node-async-testing.git" 10 | } 11 | , "bugs": { "web": "http://github.com/bentomas/node-async-testing/issues" } 12 | , "licenses": 13 | [ { "type": "MIT" 14 | , "url": "http://github.com/bentomas/node-async-testing/blob/master/LICENSE" 15 | } 16 | ] 17 | , "dependencies": {} 18 | } 19 | 20 | -------------------------------------------------------------------------------- /test/flow/flow-raw.js: -------------------------------------------------------------------------------- 1 | // This test suite is for making sure that all the right functions are called 2 | // in the right order. We keep track of when everything is ran and then 3 | // output it all at the end. 4 | // 5 | // As such, you have to manually look at the quite cryptic output to make sure 6 | // it is doing what you want. 7 | 8 | if (module == require.main) { 9 | return require('../../lib/async_testing').run(process.ARGV, function() {/* do nothing */}); 10 | } 11 | 12 | var order = '' 13 | // Which functions should be run async or sync. Each slot is a different flow 14 | // function and whether or not you want it to be sync or async. 15 | // this gives you fine tuned control over how to run the tests. 16 | , runSyncAsync = ['S', 'S', 'S', 'S', 'S'] 17 | // specify which tests to create. P means that function should pass, 18 | // F means it should fail 19 | , tests = 20 | { AA: 'PPP' 21 | , AB: 'FPP' 22 | , AC: 'PFP' 23 | , AD: 'PPF' 24 | , AE: 'FPF' 25 | , AF: 'PFF' 26 | , AG: 'PPPPP' 27 | , AH: 'FPPPP' 28 | , AI: 'PFPPP' 29 | , AJ: 'PPFPP' 30 | , AK: 'PPPFP' 31 | , AL: 'PPPPF' 32 | , AM: 'FPPPF' 33 | , AN: 'PFPFP' 34 | , AO: 'PFPPF' 35 | , AP: 'PPFFP' 36 | , AQ: 'PPFPF' 37 | , AR: 'PPPFF' 38 | , AS: 'EPP' 39 | , AT: 'PEP' 40 | , AU: 'PPE' 41 | , AV: 'EPPPP' 42 | , AW: 'PEPPP' 43 | , AX: 'PPEPP' 44 | , AY: 'PPPEP' 45 | , AZ: 'PPPPE' 46 | , BA: 'EEP' 47 | , BB: 'EPE' 48 | , BC: 'PEE' 49 | , BD: 'EFP' 50 | , BE: 'EPF' 51 | , BF: 'PEF' 52 | , BG: 'FEP' 53 | , BH: 'FPE' 54 | , BI: 'PFE' 55 | } 56 | ; 57 | 58 | for (var key in tests) { 59 | combinations(tests[key]).forEach(function(t) { 60 | var k = key; 61 | var test = []; 62 | var name = ''; 63 | 64 | t.forEach(function(f) { 65 | var index = test.length + 1; 66 | 67 | if (f == 'P') { 68 | name += 'pass '; 69 | test.push(function(t) { 70 | order += (index == 1 ? '\n' : '')+k+index; 71 | t.finish(); 72 | }); 73 | } 74 | else if (f == 'p') { 75 | name += 'apass '; 76 | test.push(function(t) { 77 | setTimeout(function() { 78 | order += (index == 1 ? '\n' : '')+k.toLowerCase()+index; 79 | t.finish(); 80 | }, 10); 81 | }); 82 | } 83 | else if (f == 'E') { 84 | name += 'error '; 85 | test.push(function(t) { 86 | order += (index == 1 ? '\n' : '')+k+index+'!'; 87 | throw new Error('error in '+index); 88 | }); 89 | } 90 | else if (f == 'e') { 91 | name += 'aerror '; 92 | test.push(function(t) { 93 | setTimeout(function() { 94 | order += (index == 1 ? '\n' : '')+k.toLowerCase()+index+'!'; 95 | throw new Error('error in '+index); 96 | }, 10); 97 | }); 98 | } 99 | else if (f == 'F') { 100 | name += 'fail '; 101 | test.push(function(t) { 102 | order += (index == 1 ? '\n' : '')+k+index+'*'; 103 | t.ok(false, 'failure in '+index); 104 | }); 105 | } 106 | else { 107 | name += 'afail '; 108 | test.push(function(t) { 109 | setTimeout(function() { 110 | order += (index == 1 ? '\n' : '')+k.toLowerCase()+index+'*'; 111 | t.ok(false, 'failure in '+index); 112 | }, 10); 113 | }); 114 | } 115 | }); 116 | 117 | exports[name.trim()] = test; 118 | }); 119 | } 120 | 121 | function combinations(list, spot) { 122 | if (!spot) { 123 | spot = 0; 124 | } 125 | if (list.length > 1) { 126 | var right = combinations(list.slice(1), spot+1); 127 | var r = []; 128 | 129 | for (var i = 0; i < right.length; i++) { 130 | if (list[0] == 'E' || runSyncAsync[spot] == 'S') { 131 | r.push([list[0]].concat(right[i])); 132 | } 133 | else { 134 | r.push([list[0].toLowerCase()].concat(right[i])); 135 | } 136 | } 137 | 138 | return r; 139 | } 140 | else { 141 | var r = []; 142 | 143 | if (list[0] == 'E' || runSyncAsync[spot] == 'S') { 144 | r.push(list[0]); 145 | } 146 | else { 147 | r.push(list[0].toLowerCase()); 148 | } 149 | return r; 150 | } 151 | } 152 | 153 | 154 | setTimeout(function() { 155 | console.log(order); 156 | }, 500); 157 | -------------------------------------------------------------------------------- /test/flow/flow-wrap.js: -------------------------------------------------------------------------------- 1 | // This test suite is for making sure that all the right functions are called 2 | // in the right order. We keep track of when everything is ran and then 3 | // output it all at the end. 4 | // 5 | // As such, you have to manually look at the quite cryptic output to make sure 6 | // it is doing what you want. 7 | 8 | if (module == require.main) { 9 | return require('../../lib/async_testing').run(process.ARGV, function() {/* do nothing */}); 10 | } 11 | 12 | var async_testing = require('../../lib/async_testing') 13 | , wrap = async_testing.wrap 14 | ; 15 | 16 | var async = true; 17 | 18 | var order = '' 19 | // specify which tests to create. P means that function should pass, 20 | // F means it should fail 21 | , tests = 22 | { A: 'P' 23 | , B: 'F' 24 | , sA: 25 | { C: 'P' 26 | , D: 'F' 27 | , sB: 28 | { E: 'P' 29 | , F: 'F' 30 | } 31 | , sC: 32 | { TS: 'F' 33 | , G: 'F' 34 | } 35 | , sD: 36 | { TT: 'F' 37 | , H: 'P' 38 | , I: 'F' 39 | } 40 | } 41 | , sE: 42 | { TS: 'F' 43 | , sF: 44 | { J: 'P' 45 | , K: 'F' 46 | } 47 | } 48 | , sG: 49 | { TT: 'F' 50 | , sH: 51 | { L: 'P' 52 | , M: 'F' 53 | } 54 | } 55 | , sI: 56 | { N: 'P' 57 | , O: 'F' 58 | } 59 | , P: 'E' 60 | , sJ: 61 | { Q: 'E' 62 | , R: 'P' 63 | , S: 'F' 64 | , sK: 65 | { TS: 'E' 66 | , T: 'F' 67 | } 68 | , sL: 69 | { TT: 'E' 70 | , U: 'P' 71 | , V: 'F' 72 | } 73 | } 74 | , sM: 75 | { SS: 'E' 76 | , sN: 77 | { W: 'P' 78 | , X: 'F' 79 | } 80 | } 81 | , sO: 82 | { ST: 'E' 83 | , sP: 84 | { Y: 'P' 85 | , Z: 'F' 86 | } 87 | } 88 | , sQ: 89 | { TS: 'E' 90 | , sR: 91 | { A: 'P' 92 | , B: 'F' 93 | } 94 | } 95 | , sS: 96 | { TT: 'E' 97 | , sT: 98 | { C: 'P' 99 | , D: 'F' 100 | } 101 | } 102 | }; 103 | 104 | var specialKeys = 105 | { SS: 'suiteSetup' 106 | , TS: 'setup' 107 | , TT: 'teardown' 108 | , ST: 'suiteTeardown' 109 | }; 110 | var symbol = 111 | { 'P': '' 112 | , 'E': '!' 113 | , 'F': '*' 114 | }; 115 | var funcs = 116 | { SS: function(prefix, key, state) { 117 | return function(d) { 118 | if (async) { 119 | setTimeout(doIt, 10); 120 | } 121 | else { 122 | doIt(); 123 | } 124 | 125 | function doIt() { 126 | order += prefix + key + '0' + symbol[state] + '\n'; 127 | if (state == 'E') { 128 | throw new Error('error in 0'); 129 | } 130 | else { 131 | d(); 132 | } 133 | } 134 | } 135 | } 136 | , TS: function(prefix, key, state) { 137 | return function(t, f) { 138 | if (state != 'E' && async) { 139 | setTimeout(doIt, 10); 140 | } 141 | else { 142 | doIt(); 143 | } 144 | 145 | function doIt() { 146 | order += prefix + ' ' + key + '1' + symbol[state] + '\n'; 147 | if (state == 'E') { 148 | throw new Error('error in 1'); 149 | } 150 | else if (state == 'P') { 151 | f(); 152 | } 153 | else { 154 | t.ok(false, 'failure in 1'); 155 | } 156 | } 157 | } 158 | } 159 | , TT: function(prefix, key, state) { 160 | return function(t, f) { 161 | if (state != 'E' && async) { 162 | setTimeout(doIt, 10); 163 | } 164 | else { 165 | doIt(); 166 | } 167 | 168 | function doIt() { 169 | order += prefix + ' ' + key + '3' + symbol[state] + '\n'; 170 | if (state == 'E') { 171 | throw new Error('error in 3'); 172 | } 173 | else if (state == 'P') { 174 | f(); 175 | } 176 | else { 177 | t.ok(false, 'failure in 3'); 178 | } 179 | } 180 | } 181 | } 182 | , ST: function(prefix, key, state) { 183 | return function(d) { 184 | if (async) { 185 | setTimeout(doIt, 10); 186 | } 187 | else { 188 | doIt(); 189 | } 190 | 191 | function doIt() { 192 | order += prefix + key + '4' + symbol[state] + '\n'; 193 | if (state == 'E') { 194 | throw new Error('error in 4'); 195 | } 196 | else { 197 | d(); 198 | } 199 | } 200 | } 201 | } 202 | , TEST: function(prefix, key, state) { 203 | return function(t, f) { 204 | if (state != 'E' && async) { 205 | setTimeout(doIt, 10); 206 | } 207 | else { 208 | doIt(); 209 | } 210 | 211 | function doIt() { 212 | order += prefix + ' ' + key + '2' + symbol[state] + '\n'; 213 | if (state == 'E') { 214 | throw new Error('error in 2'); 215 | } 216 | else if (state == 'P') { 217 | f(); 218 | } 219 | else { 220 | t.ok(false, 'failure in 2'); 221 | } 222 | } 223 | } 224 | } 225 | }; 226 | function convert(obj, p, prefix) { 227 | var wrapper = { suite: obj }; 228 | 229 | if (typeof prefix != 'string') { prefix = ''; } 230 | if (!obj.SS) { obj.SS = 'P'; } 231 | if (!obj.ST) { obj.ST = 'P'; } 232 | if (!obj.TS) { obj.TS = 'P'; } 233 | if (!obj.TT) { obj.TT = 'P'; } 234 | 235 | for (var key in obj) { 236 | if (typeof obj[key] == 'string') { 237 | var k = key in specialKeys ? p : key; 238 | obj[key] = (funcs[key] || funcs.TEST)(prefix, k, obj[key]); 239 | if (key in specialKeys) { 240 | wrapper[specialKeys[key]] = obj[key]; 241 | delete obj[key]; 242 | } 243 | } 244 | else { 245 | var c = convert(obj[key], key, prefix + ' '); 246 | wrapper.suite[key] = c; 247 | } 248 | } 249 | 250 | return wrap(wrapper); 251 | } 252 | 253 | module.exports = convert(tests, '__'); 254 | 255 | 256 | setTimeout(function() { 257 | console.log(order); 258 | }, 2200); 259 | -------------------------------------------------------------------------------- /test/test-all_passing.js: -------------------------------------------------------------------------------- 1 | if (module == require.main) { 2 | return require('../lib/async_testing').run(process.ARGV); 3 | } 4 | 5 | module.exports = { 6 | 'test A': function(test) { 7 | test.ok(true); 8 | test.finish(); 9 | }, 10 | 11 | 'test B': function(test) { 12 | test.ok(true); 13 | test.finish(); 14 | }, 15 | 16 | 'test C': function(test) { 17 | test.ok(true); 18 | test.finish(); 19 | }, 20 | 21 | 'test D': function(test) { 22 | test.ok(true); 23 | test.finish(); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /test/test-async_assertions.js: -------------------------------------------------------------------------------- 1 | if (module == require.main) { 2 | return require('../lib/async_testing').run(process.ARGV); 3 | } 4 | 5 | module.exports = { 6 | 'test success': function(test) { 7 | setTimeout(function() { 8 | test.ok(true, 'This should be true'); 9 | test.finish(); 10 | }, 500); 11 | }, 12 | 13 | 'test fail': function(test) { 14 | setTimeout(function() { 15 | test.ok(false, 'This should be false'); 16 | test.finish(); 17 | }, 500); 18 | }, 19 | 20 | 'test success -- numAssertions expected': function(test) { 21 | test.numAssertions = 1; 22 | setTimeout(function() { 23 | test.ok(true, 'This should be true'); 24 | test.finish(); 25 | }, 500); 26 | }, 27 | 28 | 'test fail -- numAssertions expected': function(test) { 29 | test.numAssertions = 1; 30 | setTimeout(function() { 31 | test.ok(false, 'fail -- numAssertions expected shouldn\'t overwrite failures'); 32 | test.finish(); 33 | }, 500); 34 | }, 35 | 36 | 'test fail - not enough -- numAssertions expected': function(test) { 37 | test.numAssertions = 1; 38 | setTimeout(function() { 39 | test.finish(); 40 | }, 500); 41 | }, 42 | 43 | 'test fail - too many -- numAssertions expected': function(test) { 44 | test.numAssertions = 1; 45 | setTimeout(function() { 46 | test.ok(true, 'This should be true'); 47 | test.ok(true, 'This should be true'); 48 | test.finish(); 49 | }, 500); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/test-child_message_interference.js: -------------------------------------------------------------------------------- 1 | if (module == require.main) { 2 | return require('../lib/async_testing').run(process.ARGV); 3 | } 4 | 5 | module.exports = { 6 | 'test interference': function(test) { 7 | test.finish() 8 | require('util').print('interference') 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/test-custom_assertions.js: -------------------------------------------------------------------------------- 1 | var async_testing = require('../lib/async_testing'); 2 | 3 | if (module == require.main) { 4 | return async_testing.run(process.ARGV); 5 | } 6 | 7 | var assert = require('assert'); 8 | 9 | async_testing.registerAssertion('isTwo', 10 | function isTwo(actual, message) { 11 | if (actual !== 2) { 12 | assert.fail(actual, 2, message, '==', isTwo); 13 | } 14 | }); 15 | 16 | module.exports = { 17 | 'test custom assertion pass': function(test) { 18 | test.isTwo(2); 19 | test.finish(); 20 | }, 21 | 22 | 'test custom assertion fail': function(test) { 23 | test.isTwo(1); 24 | test.finish(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/test-error_async.js: -------------------------------------------------------------------------------- 1 | if (module == require.main) { 2 | return require('../lib/async_testing').run(process.ARGV); 3 | } 4 | 5 | module.exports = 6 | { 'test passes': function(test) { 7 | setTimeout(function() { 8 | test.ok(true); 9 | test.finish(); 10 | }, 500); 11 | } 12 | 13 | , 'test async error 1': function(test) { 14 | setTimeout(function() { 15 | throw new Error('error 1'); 16 | }, 500); 17 | } 18 | 19 | , 'test async error 2': function(test) { 20 | setTimeout(function() { 21 | throw new Error('error 2'); 22 | }, 500); 23 | } 24 | 25 | , 'test async error 3': function(test) { 26 | setTimeout(function() { 27 | throw new Error('error 3'); 28 | }, 500); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/test-error_outside_suite.js: -------------------------------------------------------------------------------- 1 | if (module == require.main) { 2 | return require('../lib/async_testing').run(process.ARGV); 3 | } 4 | 5 | throw new Error(); 6 | -------------------------------------------------------------------------------- /test/test-error_sync.js: -------------------------------------------------------------------------------- 1 | if (module == require.main) { 2 | return require('../lib/async_testing').run(process.ARGV); 3 | } 4 | 5 | module.exports = { 6 | 'test sync error': function(test) { 7 | throw new Error(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/test-error_syntax.js: -------------------------------------------------------------------------------- 1 | ' 2 | -------------------------------------------------------------------------------- /test/test-error_test_already_finished.js: -------------------------------------------------------------------------------- 1 | if (module == require.main) { 2 | return require('../lib/async_testing').run(process.ARGV); 3 | } 4 | 5 | module.exports = { 6 | 'test sync already finished': function(test) { 7 | test.finish(); 8 | test.finish(); 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /test/test-error_test_already_finished_async.js: -------------------------------------------------------------------------------- 1 | if (module == require.main) { 2 | return require('../lib/async_testing').run(process.ARGV); 3 | } 4 | 5 | module.exports = { 6 | 'test async already finished': function(test) { 7 | test.finish(); 8 | process.nextTick(function() { 9 | test.finish(); 10 | }); 11 | }, 12 | 'test another test': function(test) { 13 | test.finish(); 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /test/test-error_uncaught_exception_handler.js: -------------------------------------------------------------------------------- 1 | if (module == require.main) { 2 | return require('../lib/async_testing').run(process.ARGV); 3 | } 4 | 5 | module.exports = { 6 | 'test sync error error again': function(test) { 7 | var e = new Error('first error'); 8 | 9 | test.uncaughtExceptionHandler = function(err) { 10 | throw new Error('second error'); 11 | } 12 | 13 | throw e; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/test-interference.js: -------------------------------------------------------------------------------- 1 | if (module == require.main) { 2 | return require('async_testing').run(process.ARGV); 3 | } 4 | 5 | require('../lib/child') 6 | 7 | exports ['test should fail'] = function (test){ 8 | test.ok(false) 9 | } 10 | 11 | /* 12 | this test will be reported to pass, when clearly it should fail. 13 | */ 14 | -------------------------------------------------------------------------------- /test/test-overview.js: -------------------------------------------------------------------------------- 1 | var async_testing = require('../lib/async_testing') 2 | , wrap = async_testing.wrap 3 | ; 4 | 5 | if (module == require.main) { 6 | return require('../lib/async_testing').run(process.ARGV); 7 | } 8 | 9 | 10 | var suiteSetupCount = 0; 11 | 12 | module.exports = 13 | { 'asynchronous test': function(test) { 14 | setTimeout(function() { 15 | // make an assertion (these are just regular assertions) 16 | test.ok(true); 17 | // finish the test 18 | test.finish(); 19 | },500); 20 | } 21 | 22 | , 'synchronous test': function(test) { 23 | test.ok(true); 24 | test.finish(); 25 | } 26 | 27 | , 'test assertions expected': function(test) { 28 | test.numAssertions = 1; 29 | 30 | test.ok(true); 31 | test.finish(); 32 | } 33 | 34 | , 'test catch async error': function(test) { 35 | var e = new Error(); 36 | 37 | test.uncaughtExceptionHandler = function(err) { 38 | test.equal(e, err); 39 | test.finish(); 40 | } 41 | 42 | setTimeout(function() { 43 | throw e; 44 | }, 500); 45 | } 46 | 47 | , 'namespace 1': 48 | { 'test A': function(test) { 49 | test.ok(true); 50 | test.finish(); 51 | } 52 | , 'test B': function(test) { 53 | test.ok(true); 54 | test.finish(); 55 | } 56 | } 57 | , 'namespace 2': 58 | { 'test A': function(test) { 59 | test.ok(true); 60 | test.finish(); 61 | } 62 | , 'test B': function(test) { 63 | test.ok(true); 64 | test.finish(); 65 | } 66 | } 67 | , 'namespace 3': 68 | { 'test A': function(test) { 69 | test.ok(true); 70 | test.finish(); 71 | } 72 | , 'test B': function(test) { 73 | test.ok(true); 74 | test.finish(); 75 | } 76 | } 77 | 78 | , 'namespace 4': 79 | { 'namespace 5': 80 | { 'namespace 6': 81 | { 'test A': function(test) { 82 | test.ok(true); 83 | test.finish(); 84 | } 85 | , 'test B': function(test) { 86 | test.ok(true); 87 | test.finish(); 88 | } 89 | } 90 | } 91 | } 92 | 93 | , 'wrapped suite': wrap( 94 | { suiteSetup: function(done) { 95 | suiteSetupCount++; 96 | done(); 97 | } 98 | , setup: function(test, done) { 99 | test.extra1 = 1; 100 | test.extra2 = 2; 101 | done(); 102 | } 103 | , suite: 104 | { 'wrapped test 1': function(test) { 105 | test.equal(1, suiteSetupCount); 106 | test.equal(1, test.extra1); 107 | test.equal(2, test.extra2); 108 | test.finish(); 109 | } 110 | , 'wrapped test 2': function(test) { 111 | test.equal(1, suiteSetupCount); 112 | test.equal(1, test.extra1); 113 | test.equal(2, test.extra2); 114 | test.finish(); 115 | } 116 | } 117 | , teardown: function(test, done) { 118 | // not that you need to delete these variables here, they'll get cleaned up 119 | // automatically, we're just doing it here as an example of running code 120 | // after some tests 121 | delete test.extra1; 122 | delete test.extra2; 123 | done(); 124 | } 125 | , suiteTeardown: function(done) { 126 | delete suiteSetupCount; 127 | done(); 128 | } 129 | }) 130 | } 131 | -------------------------------------------------------------------------------- /test/test-parse_run_arguments.js: -------------------------------------------------------------------------------- 1 | if (module == require.main) { 2 | return require('../lib/async_testing').run(process.ARGV); 3 | } 4 | 5 | var parse = require('../lib/running').parseRunArguments; 6 | 7 | var flags = 8 | { 'group': 9 | [ { longFlag: 'first' 10 | } 11 | , { longFlag: 'flag-with-dashes' 12 | } 13 | , { longFlag: 'single' 14 | , takesValue: 'number' 15 | } 16 | , { longFlag: 'multiple' 17 | , takesValue: 'number' 18 | , multiple: true 19 | } 20 | , { longFlag: 'key' 21 | , key: 'keyed' 22 | } 23 | , { longFlag: 'value' 24 | , value: 42 25 | } 26 | , { longFlag: 'value0' 27 | , value: 0 28 | } 29 | , { longFlag: 'value-key' 30 | , key: 'keyedValued' 31 | , value: 10 32 | } 33 | ] 34 | }; 35 | 36 | module.exports = 37 | { 'test string': function(test) { 38 | var l = []; 39 | var o = {}; 40 | 41 | parse(['name'], l, o, flags); 42 | 43 | test.deepEqual(['name'], l); 44 | test.deepEqual({}, o); 45 | test.finish(); 46 | } 47 | , 'test object': function(test) { 48 | var l = []; 49 | var o = {}; 50 | 51 | parse([{first: true}], l, o, flags); 52 | 53 | test.deepEqual([], l); 54 | test.deepEqual({first: true}, o); 55 | test.finish(); 56 | } 57 | , 'test array': function(test) { 58 | var l = []; 59 | var o = {}; 60 | 61 | parse([['name', '--first']], l, o, flags); 62 | 63 | test.deepEqual({first: true}, o); 64 | test.finish(); 65 | } 66 | , 'test order1': function(test) { 67 | var l = []; 68 | var o = {}; 69 | 70 | parse([{first: false}, ['--first']], l, o, flags); 71 | 72 | test.deepEqual({first: true}, o); 73 | test.finish(); 74 | } 75 | , 'test order2': function(test) { 76 | var l = []; 77 | var o = {}; 78 | 79 | parse([['--first'], {first: false}], l, o, flags); 80 | 81 | test.deepEqual({first: false}, o); 82 | test.finish(); 83 | } 84 | , 'test flag -> key conversion': function(test) { 85 | var l = []; 86 | var o = {}; 87 | 88 | parse([['--flag-with-dashes']], l, o, flags); 89 | 90 | test.deepEqual({'flagWithDashes': true}, o); 91 | test.finish(); 92 | } 93 | , 'test single once': function(test) { 94 | var l = []; 95 | var o = {}; 96 | 97 | parse([['--single', 'one']], l, o, flags); 98 | 99 | test.deepEqual({'single': 'one'}, o); 100 | test.finish(); 101 | } 102 | , 'test single twice': function(test) { 103 | var l = []; 104 | var o = {}; 105 | 106 | parse([['--single', 'one', '--single', 'two']], l, o, flags); 107 | 108 | test.deepEqual({'single': 'two'}, o); 109 | test.finish(); 110 | } 111 | , 'test multiple once': function(test) { 112 | var l = []; 113 | var o = {}; 114 | 115 | parse([['--multiple', 'one']], l, o, flags); 116 | 117 | test.deepEqual({'multiple': ['one']}, o); 118 | test.finish(); 119 | } 120 | , 'test multiple twice': function(test) { 121 | var l = []; 122 | var o = {}; 123 | 124 | parse([['--multiple', 'one', '--multiple', 'two']], l, o, flags); 125 | 126 | test.deepEqual({'multiple': ['one','two']}, o); 127 | test.finish(); 128 | } 129 | , 'test key': function(test) { 130 | var l = []; 131 | var o = {}; 132 | 133 | parse([['--key']], l, o, flags); 134 | 135 | test.deepEqual({'keyed': true}, o); 136 | test.finish(); 137 | } 138 | , 'test value': function(test) { 139 | var l = []; 140 | var o = {}; 141 | 142 | parse([['--value']], l, o, flags); 143 | 144 | test.deepEqual({'value': 42}, o); 145 | test.finish(); 146 | } 147 | , 'test 0 value': function(test) { 148 | var l = []; 149 | var o = {}; 150 | 151 | parse([['--value0']], l, o, flags); 152 | 153 | test.deepEqual({'value0': 0}, o); 154 | test.finish(); 155 | } 156 | , 'test value and key': function(test) { 157 | var l = []; 158 | var o = {}; 159 | 160 | parse([['--value-key']], l, o, flags); 161 | 162 | test.deepEqual({'keyedValued': 10}, o); 163 | test.finish(); 164 | } 165 | }; 166 | -------------------------------------------------------------------------------- /test/test-sub_suites.js: -------------------------------------------------------------------------------- 1 | if (module == require.main) { 2 | return require('../lib/async_testing').run(process.ARGV); 3 | } 4 | 5 | module.exports = 6 | { 'sub-suite A': 7 | { 'test success': function(test) { 8 | test.ok(true, 'A success'); 9 | test.finish(); 10 | } 11 | , 'test fail': function(test) { 12 | test.ok(false, 'A fail'); 13 | test.finish(); 14 | } 15 | } 16 | , 'sub-suite B': 17 | { 'test success': function(test) { 18 | test.ok(true, 'B success'); 19 | test.finish(); 20 | } 21 | , 'test fail': function(test) { 22 | test.ok(false, 'B fail'); 23 | test.finish(); 24 | } 25 | } 26 | , 'sub-suite C': 27 | { 'test success': function(test) { 28 | test.ok(true, 'C success'); 29 | test.finish(); 30 | } 31 | , 'test fail': function(test) { 32 | test.ok(false, 'C fail'); 33 | test.finish(); 34 | } 35 | , 'sub': 36 | { 'test success': function(test) { 37 | test.ok(true, 'C sub success'); 38 | test.finish(); 39 | } 40 | , 'test fail': function(test) { 41 | test.ok(false, 'C sub fail'); 42 | test.finish(); 43 | } 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /test/test-sync_assertions.js: -------------------------------------------------------------------------------- 1 | if (module == require.main) { 2 | return require('../lib/async_testing').run(process.ARGV); 3 | } 4 | 5 | module.exports = { 6 | 'test success': function(test) { 7 | test.ok(true, 'This should be true'); 8 | test.finish(); 9 | }, 10 | 11 | 'test fail': function(test) { 12 | test.ok(false, 'This should be false'); 13 | test.finish(); 14 | }, 15 | 16 | 'test success -- numAssertionsExpected': function(test) { 17 | test.numAssertions = 1; 18 | test.ok(true, 'This should be true'); 19 | test.finish(); 20 | }, 21 | 22 | 'test fail -- numAssertionsExpected': function(test) { 23 | test.numAssertions = 1; 24 | test.ok(false, 'fail -- numAssertions expected shouldn\'t overwrite failures'); 25 | test.finish(); 26 | }, 27 | 28 | 'test fail - not enough -- numAssertionsExpected': function(test) { 29 | test.numAssertions = 1; 30 | test.finish(); 31 | }, 32 | 33 | 'test fail - too many -- numAssertionsExpected': function(test) { 34 | test.numAssertions = 1; 35 | test.ok(true, 'This should be true'); 36 | test.ok(true, 'This should be true'); 37 | test.finish(); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /test/test-uncaught_exception_handlers.js: -------------------------------------------------------------------------------- 1 | if (module == require.main) { 2 | return require('../lib/async_testing').run(process.ARGV); 3 | } 4 | 5 | module.exports = { 6 | 'test catch sync error': function(test) { 7 | var e = new Error(); 8 | 9 | test.uncaughtExceptionHandler = function(err) { 10 | test.equal(e, err); 11 | test.finish(); 12 | } 13 | 14 | throw e; 15 | }, 16 | 17 | 'test catch async error': function(test) { 18 | var e = new Error(); 19 | 20 | test.uncaughtExceptionHandler = function(err) { 21 | test.equal(err, e); 22 | test.finish(); 23 | } 24 | 25 | setTimeout(function() { 26 | throw e; 27 | }, 500); 28 | }, 29 | 30 | 'test sync error fail': function(test) { 31 | var e = new Error(); 32 | 33 | test.uncaughtExceptionHandler = function(err) { 34 | test.ok(false, 'this fails synchronously'); 35 | test.finish(); 36 | } 37 | 38 | throw e; 39 | }, 40 | 41 | 'test async error fail': function(test) { 42 | var e = new Error(); 43 | 44 | test.uncaughtExceptionHandler = function(err) { 45 | test.ok(false, 'this fails synchronously'); 46 | test.finish(); 47 | } 48 | 49 | setTimeout(function() { 50 | throw e; 51 | }, 500); 52 | }, 53 | 54 | 'test sync error async fail': function(test) { 55 | var e = new Error(); 56 | 57 | test.uncaughtExceptionHandler = function(err) { 58 | setTimeout(function() { 59 | test.ok(false, 'this fails asynchronously'); 60 | test.finish(); 61 | }, 500); 62 | } 63 | 64 | throw e; 65 | }, 66 | 67 | 'test async error async fail': function(test) { 68 | var e = new Error(); 69 | 70 | test.uncaughtExceptionHandler = function(err) { 71 | setTimeout(function() { 72 | test.ok(false, 'this fails asynchronously'); 73 | test.finish(); 74 | }, 500); 75 | } 76 | 77 | setTimeout(function() { 78 | throw e; 79 | }, 500); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/test-weird_throws.js: -------------------------------------------------------------------------------- 1 | if (module == require.main) { 2 | return require('../lib/async_testing').run(process.ARGV); 3 | } 4 | 5 | module.exports = { 6 | 'test throw null': function(test) { 7 | throw null; 8 | } 9 | , 'test throw undefined': function(test) { 10 | throw undefined; 11 | } 12 | , 'test throw boolean': function(test) { 13 | throw false; 14 | } 15 | , 'test throw number': function(test) { 16 | throw 0; 17 | } 18 | , 'test throw String': function(test) { 19 | throw 'hello'; 20 | } 21 | /*, 'test throw error without stack': function(test) { 22 | //weird but it happens some times... stackoverflow, for example. 23 | var e = new Error ("THIS ERROR HAS NO STACK TRACE") 24 | delete e.stack 25 | throw e 26 | }*/ 27 | , 'test stack overflow': function(test) { 28 | //weird but it happens some times... stackoverflow, for example. 29 | function overflow(){ 30 | overflow() 31 | } 32 | overflow() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/test-wrap_tests.js: -------------------------------------------------------------------------------- 1 | var async_testing = require('../lib/async_testing') 2 | , wrap = async_testing.wrap 3 | ; 4 | 5 | if (module == require.main) { 6 | return async_testing.run(process.ARGV); 7 | } 8 | 9 | var extra1 = {}, extra2 = {}; 10 | 11 | module.exports = { 12 | 'sync wrap': wrap( 13 | { suite: 14 | { 'test': function(test) { 15 | test.numAssertions = 5; 16 | test.strictEqual(test.one, extra1); 17 | test.strictEqual(test.two, extra2); 18 | test.finish(); 19 | } 20 | } 21 | , suiteSetup: function(done) { 22 | done(); 23 | } 24 | , setup: function setup(test, done) { 25 | test.ok(true, 'make sure we run the setup'); 26 | test.one = extra1; 27 | test.two = extra2; 28 | 29 | done(); 30 | } 31 | , teardown: function teardown(test, done) { 32 | test.ok(true, 'make sure we run the teardown'); 33 | test.one = extra1; 34 | test.two = extra2; 35 | 36 | done(); 37 | } 38 | }), 39 | 40 | 'async setup': wrap( 41 | { suite: 42 | { 'test': function(test) { 43 | test.numAssertions = 4; 44 | test.strictEqual(test.one, extra1); 45 | test.strictEqual(test.two, extra2); 46 | test.finish(); 47 | } 48 | } 49 | , setup: function setup(test, done) { 50 | test.ok(true, 'make sure we run the setup'); 51 | test.one = extra1; 52 | test.two = extra2; 53 | 54 | setTimeout(done, 500); 55 | } 56 | }), 57 | 58 | 'async teardown': wrap( 59 | { suite: 60 | { 'test': function(test) { 61 | test.numAssertions = 2; 62 | test.finish(); 63 | } 64 | } 65 | , teardown: function teardown(test, done) { 66 | test.ok(true, 'make sure we run the teardown'); 67 | 68 | setTimeout(done, 500); 69 | } 70 | }), 71 | }; 72 | 73 | wrap( { suite: module.exports 74 | , setup: function(test, done) { 75 | test.ok(true, 'make sure we get to outer level setup') 76 | 77 | done(); 78 | } 79 | }); 80 | 81 | 82 | if (module == require.main) { 83 | async_testing.run(__filename, process.ARGV); 84 | } 85 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | Featueres (not sorted in order of importance 2 | -------------------------------------------- 3 | async_testing.run: 4 | + better error handling when parsing command line arguments, and by better 5 | I mean, any 6 | + help message summarizing async_testing in generateHelp 7 | ? onTestSkipped event for when a test is skipped because a specific test was 8 | specified? 9 | ? stop using home grown options parser and add one as a sub module? 10 | ? allow a config file (at say ~/.node-async-testing.json) or something for 11 | setting default run options, so say if you want the default to have tests and 12 | suites be parallel you can do that. 13 | ? make the default to be to run test and suites in parallel? 14 | ? Add new flag, which says to run everything in parallel, but if a suite fails 15 | in some way, don't output it's results, instead re-run the suite serially 16 | 17 | Console Runner: 18 | + deal with long lines better (wrapping function) 19 | 20 | Web Runner: 21 | + checkbox for web runner to automatically run suites on window or tab focus 22 | + keep track of which suites have been opened and are parallel across refreshes 23 | (in a cookie) 24 | + checkbox to run suites in parallel or not (right now you have to specify this <---- 25 | via the command line) 26 | + improve UI for when onSuiteDone with an 'error' status happens 27 | ? only show suites that have tests we are running and only show those tests (in <---- 28 | the case of the --test-name flag) 29 | ? Instead of just show test as blank when a file changes, mention something? 30 | ? Show number of failures when the test is closed? 31 | ? better support for onSuiteExit event. Show which tests didn't finish and 32 | which didn't get ran 33 | 34 | Running tests (async_testing.runSuite, async_testing.runFile): 35 | + timeout for suites or tests, the easiest and most fool proof way would be to 36 | just add this to runFile and just have it kill the process after a certain 37 | amount of time. It could look at the events it is getting from the child and 38 | restart the timeout every time a test finishes. If we want to do this in 39 | runSuite I don't think there is anything we can do to about something like 40 | this happening (since node is single threaded and callbacks won't interrupt 41 | running code): 42 | `while(true) {}` 43 | + improve stack traces for assertion failures (remove first line, which is just 44 | the wrapped assertion being called) 45 | + code coverage 46 | ? test.finish can take error? so you could say do: 47 | `fs.readFile(test.finish)` 48 | to make sure that readFile doesn't error without having to write your 49 | own callback 50 | ? add a script for running tests? (like we used to have...). People seem to like 51 | them, and it would allow us to specify a directory not always having to specify 52 | a file. 53 | 54 | Docs 55 | ---- 56 | + update docs and add list of command line arguments for run and what they do 57 | + add note about contributing/contacting 58 | --------------------------------------------------------------------------------