1 | **due homage:** [RSpec](http://rspec.info/)
|
2 |
|
3 | **experimental/unstable** api changes will still occur (**without** deprecation warnings)
|
4 |
|
5 | `npm install ipso` 0.0.18 [license](./license)
|
6 |
|
7 |
|
8 | Injection Decorator, for mocking and stubbing, with [Mocha](https://github.com/visionmedia/mocha)
|
9 |
|
10 | Almost all examples in [coffee-script](http://coffeescript.org/).
|
11 |
|
12 |
|
13 | What is this `ipso` thing?
|
14 | --------------------------
|
15 |
|
16 | [The Short Answer](https://github.com/nomilous/vertex/commit/a4b0ef4c6bc14874f5b7d8ff3e5bcbcf4d45edc6)
|
17 |
|
18 | The Long Answer, ↓
|
19 |
|
20 | ### (test/) Injection Decorator
|
21 |
|
22 | It is placed in front of the test functions.
|
23 |
|
24 | ```coffee
|
25 | ipso = require 'ipso'
|
26 |
|
27 | it 'does something', ipso (done) ->
|
28 |
|
29 | done() # as usual
|
30 |
|
31 | ```
|
32 |
|
33 | or js:
|
34 |
|
35 | ```js
|
36 | ipso = require('ipso');
|
37 |
|
38 | it('does something', ipso( function(done) {
|
39 |
|
40 | done();
|
41 |
|
42 | } ));
|
43 |
|
44 | ```
|
45 |
|
46 | It can inject node modules into suites.
|
47 |
|
48 | ```coffee
|
49 |
|
50 | describe 'it can inject into describe', ipso (vm) ->
|
51 | context 'it can inject into context', ipso (net) ->
|
52 | it 'confirms', ->
|
53 |
|
54 | vm.should.equal require 'vm'
|
55 | net.should.equal require 'net'
|
56 |
|
57 | ```
|
58 |
|
59 | It can inject node modules into tests.
|
60 |
|
61 | ```coffee
|
62 |
|
63 | it 'does something', ipso (done, http) ->
|
64 |
|
65 | http.should.equal require 'http'
|
66 |
|
67 | ```
|
68 |
|
69 | **IMPORTANT**: `done` will only contain the test resolver if the argument's signaure is literally "done" and it in the first position.
|
70 |
|
71 | In other words.
|
72 |
|
73 | ```coffee
|
74 |
|
75 | it 'does something', ipso (finished, http) ->
|
76 |
|
77 | #
|
78 | # => Error: Cannot find module 'finished'
|
79 | #
|
80 | # And the problem becomes more subtle if there IS a module called 'finshed' installed...
|
81 | #
|
82 |
|
83 | ```
|
84 |
|
85 |
|
86 | It defines `.does()` on each injected module for use as a **stubber**.
|
87 |
|
88 | ```coffee
|
89 |
|
90 | it 'creates an http server', ipso (done, http) ->
|
91 |
|
92 | http.does
|
93 | createServer: ->
|
94 | anotherFunction: ->
|
95 |
|
96 | http.createServer()
|
97 | done()
|
98 |
|
99 | ```
|
100 |
|
101 | It uses mocha's JSON diff to display failure to call the stubbed function.
|
102 |
|
103 | ```json
|
104 |
|
105 | actual expected
|
106 |
|
107 | 1 | {
|
108 | 2 | "http": {
|
109 | 3 | "functions": {
|
110 | 4 | "Object.createServer()": "was called"
|
111 | 5 | "Object.anotherFunction()": "was NOT called"
|
112 | 6 | }
|
113 | 7 | }
|
114 | 8 | }
|
115 |
|
116 | ```
|
117 |
|
118 | or, (depending on your mocha version)
|
119 |
|
120 | ```json
|
121 |
|
122 | + expected - actual
|
123 |
|
124 | {
|
125 | "http": {
|
126 | "functions": {
|
127 | "Object.createServer()": "was called",
|
128 | + "Object.anotherFunction()": "was called"
|
129 | - "Object.anotherFunction()": "was NOT called"
|
130 | }
|
131 | }
|
132 | }
|
133 |
|
134 | ```
|
135 |
|
136 | The stub replaces the actual function on the module and can therefore return a suitable mock.
|
137 |
|
138 | ```coffee
|
139 | http = require 'http'
|
140 | class MyServer
|
141 | listen: (opts, handler) ->
|
142 | http.createServer(handler).listen opts.port
|
143 | ```
|
144 |
|
145 | ```coffee
|
146 | {ipso, mock} = require 'ipso'
|
147 |
|
148 | it 'creates an http server and listens at opts.port', ipso (done, http, MyServer) ->
|
149 |
|
150 | http.does
|
151 | createServer: ->
|
152 | return mock('server').does
|
153 | listen: (port) ->
|
154 | port.should.equal 3000
|
155 | done()
|
156 |
|
157 | MyServer.listen port: 3000, (req, res) ->
|
158 |
|
159 | ```
|
160 |
|
161 | You may have noticed that `MyServer` was also injected in the previous example.
|
162 |
|
163 | * The injector recurses `./lib` and `./app` for the specified module.
|
164 | * It does so only if the module has a `CamelCaseModuleName` in the injection argument's signature
|
165 | * It searches for the underscored equivalent `./lib/**/*/camel_case_module_name.js|coffee`
|
166 | * TODO: make search strategy configurable
|
167 | * These **Local Module Injections** can also be stubbed.
|
168 |
|
169 |
|
170 | It can create multiple function expectation stubs ( **and spies** ).
|
171 |
|
172 | ```coffee
|
173 |
|
174 | it 'can create multiple expectation stubs', ipso (done, Server) ->
|
175 |
|
176 | Server.does
|
177 |
|
178 | _listen: ->
|
179 |
|
180 | # console.log arguments
|
181 |
|
182 | console.log """
|
183 |
|
184 | _underscore denotes a spy function
|
185 | ==================================
|
186 |
|
187 | * the original will be called after the spy (this function)
|
188 | * both will receive the same arguments
|
189 |
|
190 | """
|
191 |
|
192 | anotherFunction: ->
|
193 |
|
194 | Server.start()
|
195 |
|
196 |
|
197 | ```
|
198 |
|
199 | **IMPORTANT** Stubs set up in before (All) hooks are not enforced as expectations
|
200 |
|
201 | ```coffee
|
202 |
|
203 | {ipso, mock} = require 'ipso'
|
204 |
|
205 |
|
206 | before ipso ->
|
207 | mock('thing').does
|
208 | function1: -> return 'value1'
|
209 |
|
210 |
|
211 | beforeEach ipso (thing) ->
|
212 |
|
213 | #
|
214 | # injected mock thing (as defined in above)
|
215 | #
|
216 |
|
217 | thing.does
|
218 | function2: -> return 'value2'
|
219 |
|
220 |
|
221 |
|
222 | it 'calls function2', ipso (thing) ->
|
223 |
|
224 | thing.function2()
|
225 |
|
226 | #
|
227 | # does not fail even tho function1() was not called
|
228 | #
|
229 |
|
230 |
|
231 | ```
|
232 |
|
233 | Mocks can define properties using `.with()`
|
234 |
|
235 | ```coffee
|
236 |
|
237 | {ipso, mock} = require 'ipso'
|
238 |
|
239 | before ipso ->
|
240 | mock('thing').with
|
241 | property1: 'value1'
|
242 | property2: 'value2'
|
243 |
|
244 | beforeEach ipso (thing) ->
|
245 |
|
246 | thing.with
|
247 |
|
248 | property2: 'overwrite value2'
|
249 |
|
250 | .does
|
251 |
|
252 | function1: -> 'with and does are chainable'
|
253 | function2: ->
|
254 |
|
255 |
|
256 | ```
|
257 |
|
258 | * Note that `.with()` only exists on objects created with ipso.mock()
|
259 |
|
260 |
|
261 | **PENDING (unlikely, use tags, see below)** It can create future instance stubs (on the prototype)
|
262 |
|
263 | ```coffee
|
264 |
|
265 | it 'can create multiple expectation stubs', ipso (done, Periscope, events, should) ->
|
266 |
|
267 | # Periscope.$prototype.does (dunno yet)
|
268 | Periscope.prototype.does
|
269 |
|
270 | measureDepth: -> return 30
|
271 |
|
272 | _riseToSurface: (distance, finishedRising) ->
|
273 | distance.should.equal 30
|
274 |
|
275 | _openLens: ->
|
276 | @videoStream.codec.should.equal πr²
|
277 |
|
278 | #
|
279 | # note: That `@` a.k.a. `this` refers to the instance context
|
280 | # and not the test context. It therefore has access to
|
281 | # properties of the Periscope instance.
|
282 | #
|
283 |
|
284 |
|
285 | periscope = new Periscope codec: πr²
|
286 | periscope.up (error, eyehole) ->
|
287 |
|
288 | should.not.exist error
|
289 | eyehole.should.be.an.instanceof events.EventEmitter
|
290 | done()
|
291 |
|
292 | ```
|
293 |
|
294 | It supports taging objects for multiple subsequent injections by tag.
|
295 |
|
296 | ```coffee
|
297 |
|
298 | context 'creates tagged objects for injection into multiple nested tests', ->
|
299 |
|
300 | before ipso (ClassName) ->
|
301 |
|
302 | ipso.tag
|
303 |
|
304 | instanceA: new ClassName 'type A'
|
305 | instanceB: new ClassName 'type B'
|
306 | client: require 'socket.io-client'
|
307 |
|
308 | it 'can test with them', (instanceA, instanceB, client) ->
|
309 | it 'and again', (instanceA, instanceB) ->
|
310 |
|
311 | ```
|
312 |
|
313 |
|
314 | ### Complex Usage
|
315 |
|
316 |
|
317 | It can create active mocks for fullblown mocking and stubbing
|
318 |
|
319 | ```coffee
|
320 |
|
321 | beforeEach ipso (done, http) ->
|
322 |
|
323 | http.does
|
324 | createServer: (handler) =>
|
325 | process.nextTick ->
|
326 |
|
327 | #
|
328 | # mock an actual "hit"
|
329 | #
|
330 |
|
331 | handler mock('req'), mock('mock response').does
|
332 |
|
333 | writeHead: ->
|
334 | write: ->
|
335 | end: ->
|
336 |
|
337 | return ipso.mock( 'mock server' ).does
|
338 |
|
339 | listen: (@port, args...) =>
|
340 | address: -> 'mock address object'
|
341 |
|
342 | #
|
343 | # note: '=>' pathway from hook's root scope means @port
|
344 | # refers to the `this` of the hook's root scope - which
|
345 | # is shared with the tests themselves, so @port becomes
|
346 | # available in all tests that are preceeded by this hook
|
347 | #
|
348 |
|
349 | it 'creates a server, starts listening and responds when hit', ipso (facto, http) ->
|
350 |
|
351 | server = http.createServer (req, res) ->
|
352 |
|
353 | res.writeHead 200
|
354 | res.end()
|
355 | facto()
|
356 |
|
357 | server.listen 3000
|
358 | @port.should.equal 3000
|
359 |
|
360 | ```
|
361 | ```json
|
362 |
|
363 | actual expected
|
364 |
|
365 | 1 | {
|
366 | 2 | "http": {
|
367 | 3 | "functions": {
|
368 | 4 | "Object.createServer()": "was called"
|
369 | 5 | }
|
370 | 6 | },
|
371 | 7 | "mock server": {
|
372 | 8 | "functions": {
|
373 | 9 | "Object.listen()": "was called",
|
374 | 10 | "Object.address()": "was called"
|
375 | 11 | }
|
376 | 12 | },
|
377 | 13 | "mock response": {
|
378 | 14 | "functions": {
|
379 | 15 | "Object.writeHead()": "was called",
|
380 | 16 | "Object.write()": "was NOT called", <--------------------
|
381 | 17 | "Object.end()": "was called"
|
382 | 18 | }
|
383 | 19 | }
|
384 | 20 | }
|
385 |
|
386 | ```
|
387 |
|
388 |
|
389 | It can **create** entire module stubs
|
390 |
|
391 | ```coffee
|
392 | {ipso, mock, Mock, define} = require 'ipso'
|
393 |
|
394 | before ipso ->
|
395 |
|
396 | #
|
397 | # create a mock to be returned by the module function
|
398 | #
|
399 |
|
400 | mock( 'nonExistant' ).with
|
401 |
|
402 | function1: ->
|
403 | property1: 'value1'
|
404 |
|
405 |
|
406 | #
|
407 | # define(listOfFunctions)
|
408 | # -----------------------
|
409 | #
|
410 | # * Keys from the list become module names
|
411 | # * Each function is run by the module stubber
|
412 | # * The returned object is exported as the module
|
413 | #
|
414 |
|
415 | define
|
416 |
|
417 | #
|
418 | # define a module that exports two class definitions
|
419 | # --------------------------------------------------
|
420 | #
|
421 | # * Mock() (capital 'M') creates mock classes
|
422 | #
|
423 | # * .with() can be used to define a baseset of functions
|
424 | # and property stubs.
|
425 | #
|
426 | # * The mock entity can be injected by tag/name for
|
427 | # per test configuration of function expectations
|
428 | # using .does()
|
429 | #
|
430 |
|
431 | missing: ->
|
432 |
|
433 | ClassName: Mock 'ClassName'
|
434 | Another: Mock('Another').with(...)
|
435 |
|
436 | #
|
437 | # define a module that exports a single function
|
438 | # ----------------------------------------------
|
439 | #
|
440 |
|
441 | 'non-existant': -> ->
|
442 |
|
443 | #
|
444 | # * The second function becomes the exported function of the module.
|
445 | #
|
446 | # * It will be retured by `require 'non-existant'`
|
447 | #
|
448 | # * get() is defined in the module scope to enable reference
|
449 | # to mocks and tags defined in this test scope.
|
450 | #
|
451 |
|
452 | return get 'nonExistant'
|
453 |
|
454 |
|
455 |
|
456 | it "has created ability to require 'non-existant' in module being tested",
|
457 |
|
458 | ipso (nonExistant, SubClass1) ->
|
459 |
|
460 | nonExistant.does function2: ->
|
461 | non = require 'non-existant'
|
462 |
|
463 | console.log non()
|
464 |
|
465 | #
|
466 | # => { function1: [Function],
|
467 | # property1: 'value1',
|
468 | # function2: [Function] }
|
469 | #
|
470 |
|
471 |
|
472 | it "can require 'missing' and create expectations on the Class / instance",
|
473 |
|
474 | ipso (ClassName, should) ->
|
475 |
|
476 | ClassName.does
|
477 |
|
478 | constructor: (arg) -> arg.should.equal 'ARG'
|
479 | someFunction: ->
|
480 |
|
481 |
|
482 |
|
483 |
|
484 | #
|
485 | # this would generally be elsewhere (in the module being tested)
|
486 | #
|
487 |
|
488 | missing = require 'missing'
|
489 | instance = new missing.ClassName 'ARG'
|
490 | instance.someFunction()
|
491 |
|
492 |
|
493 | ```
|
494 |
|
495 | * Use case
|
496 |
|
497 | * Testing [component](http://component.io/) based clientside code without running a browser.
|
498 |
|
499 | * **IMPORTANT / WARNING**
|
500 |
|
501 | * It is a clunky interface and may change drastically.
|
502 | * It tricks `require` into loading the module by tailoring the behaviours
|
503 | of fs.readFileSync, statSync and lstatSync (a not very eloquent method...)
|
504 | * It cannot be reversed (yet), so the stub remains for the duration of the
|
505 | process that created it.
|
506 |
|
507 |
|
508 | it has been shaken, not stirred
|
509 |
|
510 |
|
511 | ```coffee
|
512 |
|
513 | {ipso, tag, define, Mock} = require '../lib/ipso'
|
514 |
|
515 | before ipso (should) ->
|
516 |
|
517 | tag
|
518 |
|
519 | Got: should.exist
|
520 | Not: should.not.exist
|
521 |
|
522 | define
|
523 |
|
524 | martini: -> Mock 'VodkaMartini'
|
525 |
|
526 |
|
527 | it 'has the vodka and the olive', ipso (VodkaMartini, Got, Not) ->
|
528 |
|
529 | VodkaMartini.with
|
530 |
|
531 | olive: true
|
532 |
|
533 | .does
|
534 |
|
535 | constructor: -> @vodka = true
|
536 | shake: ->
|
537 |
|
538 | Martini = require 'martini'
|
539 | instance = new Martini
|
540 |
|
541 | Got instance.vodka
|
542 | Got instance.olive
|
543 | Not instance.gin
|
544 |
|
545 | instance.shake()
|
546 |
|
547 | try instance.stir()
|
548 |
|
549 |
|
550 |
|
551 | #
|
552 | # ps. there is great value in using **only** local scope in tests... (!, later)
|
553 | #
|
554 |
|
555 | ```
|
556 |
|
557 |
|
558 | It supports promises.
|
559 |
|
560 | ```coffee
|
561 |
|
562 | it 'fails the test on the first rejection in the chain', ipso (facto, Module) ->
|
563 |
|
564 | Module.functionThatReturnsAPromise()
|
565 |
|
566 | .then -> Module.functionThatReturnsAPromise()
|
567 | .then -> Module.functionThatReturnsAPromise()
|
568 | .then -> Module.functionThatReturnsAPromise()
|
569 | .then -> facto()
|
570 |
|
571 | ```
|
572 |
|
573 | Ipso Facto
|
574 |
|
575 | ```coffee
|
576 |
|
577 | it 'does many things to come', ipso (facto, ...) ->
|
578 |
|
579 | facto[MetaThings]()
|
580 |
|
581 | #
|
582 | # facto() calls mocha's done() in the background
|
583 | #
|
584 |
|
585 | ```
|
586 |
|
587 | What MetaThings?
|
588 |
|
589 | * well, ... (( the brief brainstorm suggested a Planet-sized Plethora of Particularly Peachy Possibilities Perch Patiently Poised Pending a Plunge into **That** rabbit hole.
|
590 |
|
591 |
|
592 | There is a [cli](https://github.com/nomilous/ipso-cli)
|
593 |
|
594 | * It assists with the overhead of dev using coffee-script, specifically the compile.then -> runTest on changes in src/**/*
|
595 |
|
596 |
|
597 |
|
598 | And who is Unthahorsten?
|
599 |
|
600 | * And why was he doing the equivalent of standing in the equivalent of a laboratory.
|
601 |
|