oojspec 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # oojspec - Object-oriented client-side testing
2
+
3
+ `oojspec` is a test runner built on top of [Buster.js](http://busterjs.org/), focused on
4
+ integration tests. It is also a Rails engine, although you can use it in non-Rails
5
+ applications - more on that in a later topic.
6
+
7
+ It uses the same assertions and reporter and you can choose between expectations
8
+ and assertions style on your examples or even mix them up.
9
+
10
+ This is heavily inspired by another beloved tool called [RSpec](https://www.relishapp.com/rspec/),
11
+ and also takes some inspiration from Jasmine and Buster.
12
+
13
+ I really prefer writing expectations instead of assertions because I think it reads better. But
14
+ on the other hand I find expectations an overkill for things like:
15
+
16
+ ```javascript
17
+ expect(seats.length).toBeTruthy()
18
+ ```
19
+
20
+ I'd rather prefer to write:
21
+
22
+ ```javascript
23
+ assert(seats.length)
24
+ ```
25
+
26
+ # Is it production ready?
27
+
28
+ It should be, but its API is certainly going to change a lot before it becomes stable.
29
+
30
+ So I wouldn't advise you to write tons of tests with `oojspec` because you might have to rewrite
31
+ them in the future when the API changes.
32
+
33
+ On the other side, I'll be using it myself in my own projects, replacing my Jasmine specs.
34
+
35
+ # Where are its tests?
36
+
37
+ I'm still not sure on how to properly test it. For the time being, I'm writing some examples
38
+ in a separate project. I'll publish its link here as soon as I polish it.
39
+
40
+ # Why not just using Buster.js (or Jasmine.js)?
41
+
42
+ Jasmine.js doesn't support beforeAll/afterAll and that was the main motivation I started to develop
43
+ another test runner. But let's be honest, I didn't want to maintain all expectations/assertions by
44
+ myself. Also I didn't want to worry about the reporter. I just wanted to focus on the runner itself.
45
+
46
+ Mocha/Chai seemed a great alternative but I had to support older Internet Explorer in my
47
+ application while the syntax of Chai required a feature not supported by those browsers.
48
+
49
+ Then I've heard about this excellent testing tool called Buster.js. It supported beforeAll/afterAll
50
+ just like Mocha but it didn't give any guarantees about the order of execution of the tests and
51
+ I needed that guarantee for some integration tests.
52
+
53
+ But on the other side it is pretty modular and I would be able to take advantage of their
54
+ assertions and expectations syntax as well as its reporter (although I'm still not happy with
55
+ the default html reporter and I'll probably have to write a new one in the future).
56
+
57
+ As Buster.js allowed me to focus on the runner itself I decided to begin this new project.
58
+
59
+ Also I took the chance to do things the correct way in my opinion. I don't like the fact that most
60
+ test runners (all of them?) will publish `describe`, `it`, `waitsFor` etc. independently from the
61
+ context.
62
+
63
+ In contrast `oojspec` will only export `describe`. The other allowed features will be available
64
+ depending on the context. Inside a description `example`, `specify`, `it`, `xit`, `pending` and
65
+ `describe` will be available. Inside an example `expect`, `assert`, `waitsFor` and `runs` will be
66
+ available .
67
+
68
+ # CoffeeScript?! Really?!
69
+
70
+ JavaScript?! Really?!
71
+
72
+ This is me who is writing this runner and I dislike JavaScript, so please keep your preferences
73
+ for you. I won't change to pure JavaScript. Period.
74
+
75
+ # Can I test my JavaScript code with oojspec despite it being written in CS?
76
+
77
+ Of course. Why are you asking me that?
78
+
79
+ On the other hand I'd advise you to write your examples in CoffeeScript for brevity.
80
+
81
+ JavaScript example:
82
+
83
+ ```javascript
84
+ oojspec.describe('Some description', function(){
85
+ this.example('Some example', function(){
86
+ this.assert(true)
87
+ })
88
+ });
89
+ ```
90
+
91
+ CoffeeScript version:
92
+
93
+ ```coffeescript
94
+ oojspec.describe 'Some description', ->
95
+ @example 'Some example', -> @assert true
96
+ ```
97
+
98
+ Alternatively, you can use some shortcut to `this` if you're using JavaScript:
99
+
100
+ ```javascript
101
+ oojspec.describe('Some description', function(s){
102
+ s.example('Some example', function(s){
103
+ s.assert(true)
104
+ })
105
+ });
106
+ ```
107
+
108
+ But I'm not sure if this API will remain supported in the future although I don't currently have
109
+ any plans to change it.
110
+
111
+ # Usage with Rails
112
+
113
+ `oojspec` is also a Rails engine built on top of
114
+ [rails-sandbox-assets](https://github.com/rosenfeld/rails-sandbox-assets).
115
+
116
+ It takes advantage of the Rails asset pipeline to run your specs. To launch the runner, after
117
+ including `oojspec` to your Gemfile, run `rake sandbox_assets:serve`.
118
+
119
+ It will load all your specs inside `(spec|test)/javascripts/oojspec/` named `*_spec.js[.coffee]`
120
+ or `*_test.js[.coffee]`. Then run the examples by accessing http://localhost:5000/oojspec.
121
+
122
+ If you want to put your specs directly on `spec/javascripts`, add this to your application.rb:
123
+
124
+ ```ruby
125
+ config.sandbox_assets.template = 'oojspec/runner'
126
+ ```
127
+
128
+ Then you'll be able to run the specs by directly accessing http://localhost:5000.
129
+
130
+ By default this gem will expose `oojspec.describe` to the `window` object so that you can write it
131
+ directly from the top-level, but you can disable this exposition if you prefer:
132
+
133
+ ```ruby
134
+ config.sandbox_assets.options[:skipt_oojspec_expose] = true
135
+ ```
136
+
137
+ # What about non-Rails applications?
138
+
139
+ There are two ways you can use oojspec with non-Rails applications.
140
+
141
+ If you want to take advantage of the Rails asset pipeline,
142
+ [here](https://github.com/rosenfeld/jasmine_assets_enabler/tree/oojs)
143
+ is an example on how to integrate the `rails-sandbox-assets` gem (and consequently this one) to
144
+ your non-Rails application.
145
+
146
+ It is target to the `oojs` gem that is currently built on top of `rails_sandbox_jasmine` but this
147
+ is going to change and `oojs` will be built on top of `oojspec` in the future. But you can currently
148
+ just add both gems right now and ignore the Jasmine runner while I don't change `oojs`.
149
+
150
+ The other approach is to compile the source (possibly in the
151
+ [Try CoffeeScript page](http://coffeescript.org/)) and write your own custom runner HTML. Just take
152
+ the template provided by this gem as an example on how to write it.
153
+
154
+ I hope to have more free time to write more in-depth examples on how to do that in the Wiki
155
+ in the future.
156
+
157
+ # Plans for the future
158
+
159
+ There are so many but I'm not sure how long it will take to implement all of the intended
160
+ features in my spare time.
161
+
162
+ I'd like to support `given-and-when-and-then-and` style specs at some point.
163
+
164
+ Also, I'm still thinking about a more object-oriented API for the description blocks.
165
+
166
+ But I wanted to have some initial working version published soon before someone register an
167
+ `oojspec` gem before me! :)
168
+
169
+ # Contributing
170
+
171
+ I'd love to hear your opinions on the API and design of `oojspec` and of course contributions
172
+ will be very welcome if they're aligned with this project goals.
173
+
174
+ Enjoy! :)
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'Oojspec'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
@@ -0,0 +1,25 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
5
+
6
+ <title>Buster.js Test Runner</title>
7
+
8
+ <%= stylesheet_link_tag "oojspec" %>
9
+ <%= javascript_include_tag "oojspec" %>
10
+ <%= javascript_tag "oojspec.exposeAll()" unless Rails.configuration.sandbox_assets.options[:skip_oojspec_expose] %>
11
+
12
+ <% @stylesheets.each do |css| -%>
13
+ <%= stylesheet_link_tag css %>
14
+ <% end -%>
15
+
16
+ <% @tests.each do |test| -%>
17
+ <%= javascript_include_tag test %>
18
+ <% end -%>
19
+ </head>
20
+ <body>
21
+
22
+ <%= javascript_tag "oojspec.autorun()" %>
23
+ </body>
24
+ </html>
25
+
@@ -0,0 +1,256 @@
1
+ # =require buster/all
2
+
3
+ window.oojspec = new class OojspecRunner
4
+ constructor: ->
5
+ @runner = buster.create buster.eventEmitter
6
+ @descriptions = []
7
+ @assertions = buster.assertions
8
+ (logFormatter = buster.create buster.format).quoteStrings = false
9
+ @assertions.format = buster.bind logFormatter, "ascii"
10
+ @assertions.on 'pass', => @stats.assertions++
11
+ @assertions.on 'failure', => @stats.failures++
12
+ #@runner.on 'context:start', => @stats.contexts++
13
+ @runner.on 'test:timeout', => @stats.timeouts++
14
+ @runner.on 'test:error', => @stats.errors++
15
+ @runner.on 'test:deferred', => @stats.deferred++
16
+
17
+ @stats =
18
+ contexts: 0
19
+ tests: 0
20
+ assertions: 0
21
+ errors: 0
22
+ failures: 0
23
+ timeouts: 0
24
+ deferred: 0
25
+
26
+ exposeAll: -> window.describe = @describe
27
+ autorun: -> @runSpecs() unless @disableAutorun
28
+
29
+ runSpecs: ->
30
+ @reporter = buster.reporters.html.create()
31
+ @reporter.listen @runner
32
+ @runner.emit 'suite:start', name: "Specs"
33
+ @runNextDescription()
34
+
35
+ runNextDescription: =>
36
+ (@runner.emit 'suite:end', @stats; return) unless @descriptions.length
37
+ # TODO: think about non null contexts usage
38
+ @descriptions.shift().run @runner, @assertions, null, @runNextDescription
39
+
40
+ describe: (description, block)=>
41
+ @stats.contexts++ # only root descriptions will be count
42
+ @descriptions.push new Description(description, block)
43
+
44
+ class Description
45
+ RESERVED = ['beforeAll', 'before', 'after', 'afterAll', 'describe', 'context',
46
+ 'example', 'it', 'specify', 'pending', 'xit']
47
+ constructor: (@description, @block)->
48
+
49
+ run: (@runner, @assertions, @context, @onFinish, @beforeBlocks = [], @afterBlocks = [])->
50
+ @runner.emit 'context:start', name: @description
51
+ @dsl = new DescribeDsl
52
+ if c = @context
53
+ for reserved in RESERVED
54
+ try
55
+ throw new Error("'#{reserved}' method is reserved for oojspec usage only") if c[reserved]
56
+ catch e
57
+ e.name = "syntax error"
58
+ @runner.emit 'test:error', name: @description, error: e
59
+ @onDescriptionFinished(e)
60
+ c[reserved] = @dsl[reserved]
61
+ @runAround @beforeBlocks, @afterBlocks, @onDescriptionFinished, @processDescriptionBlock
62
+
63
+ onDescriptionFinished: (error)=>
64
+ if error and not error.handled
65
+ error.handled = true
66
+ @runner.emit 'test:error', { name: 'Error running describe statements', error }
67
+ @runner.emit 'context:end'
68
+ @onFinish error
69
+
70
+ runAround: (befores, afters, onFinish, block)->
71
+ new AroundBlock(befores, afters, block).run @runner, @assertions, @context, onFinish
72
+
73
+ processDescriptionBlock: (onFinish)=>
74
+ context = @context or @dsl
75
+ @block.call context, context
76
+ @runAround @dsl._beforeAllBlocks_, @dsl._afterAllBlocks_, onFinish, (@onExamplesFinished)=>
77
+ @runNextStep()
78
+
79
+ runNextStep: =>
80
+ (@onExamplesFinished(); return) unless @dsl._examples_.length
81
+ nextStep = @dsl._examples_.shift()
82
+ (@reportDeferred(nextStep.description); @runNextStep(); return) if nextStep.pending
83
+ nextTick =
84
+ if nextStep instanceof Description then => nextStep
85
+ .run @runner, @assertions, @context, @runNextStep, @dsl._beforeBlocks_, @dsl._afterBlocks_
86
+ else => # ExampleWithHooks
87
+ nextStep.run @runner, @assertions, @context, @onExampleFinished
88
+ setTimeout nextTick, 0
89
+
90
+ onExampleFinished: (error)=>
91
+ (@runNextStep(); return) unless error and not error.handled
92
+ error.handled = true
93
+ console.log error
94
+ name = @description
95
+ name += " in #{error.source}" if error.source
96
+ @runner.emit 'test:error', { name, error }
97
+ @onFinish(error)
98
+
99
+ reportDeferred: (description)-> @runner.emit 'test:deferred', name: description
100
+
101
+ class DescribeDsl
102
+ addHook = (description, block, container)->
103
+ if typeof description is 'string'
104
+ return unless block # pending hook
105
+ block.description = description
106
+ else
107
+ block = description
108
+ container.push block
109
+
110
+ constructor: ->
111
+ @_beforeAllBlocks_ = []
112
+ @_beforeBlocks_ = []
113
+ @_afterBlocks_ = []
114
+ @_afterAllBlocks_ = []
115
+ @_examples_ = []
116
+ # aliases:
117
+ @it = @specify = @example
118
+ @context = @describe
119
+ @xit = @pending
120
+
121
+ beforeAll: (description, block)=> addHook description, block, @_beforeAllBlocks_
122
+ before: (description, block)=> addHook description, block, @_beforeBlocks_
123
+ after: (description, block)=> addHook description, block, @_afterBlocks_
124
+ afterAll: (description, block)=> addHook description, block, @_afterAllBlocks_
125
+ describe: (description, block)=>
126
+ @_examples_.push new Description(description, block, @_beforeBlocks_, @_afterBlocks_)
127
+ example: (description, block)=>
128
+ @_examples_.push new ExampleWithHooks(description, @_beforeBlocks_, @_afterBlocks_, block)
129
+ pending: (description)=> @_examples_.push {description, pending: true}
130
+
131
+ class AroundBlock
132
+ constructor: (@beforeBlocks, @afterBlocks, @block)->
133
+
134
+ run: (@runner, @assertions, @context, @onFinish)->
135
+ @runGroup @beforeBlocks, ((e)=> @onBeforeError e), (wasSuccessful)=>
136
+ if wasSuccessful
137
+ @runMainBlock @block, (error)=>
138
+ @registerError error
139
+ @runAfterGroup()
140
+ else @runAfterGroup()
141
+
142
+ registerError: (error)->
143
+ @runner.emit 'oojspec:log:error', error
144
+ @error or= error
145
+
146
+ runMainBlock: (block, onFinish)->
147
+ try
148
+ block onFinish
149
+ catch error
150
+ error = new Error(error) if typeof error is 'string'
151
+ @registerError error
152
+ onFinish error
153
+
154
+ runGroup: (group, onError, onFinish)->
155
+ new ExampleGroupWithoutHooks(@assertions, @context, group, onFinish, onError).run()
156
+
157
+ onBeforeError: (error)-> error.source = "before hook"; @registerError error
158
+ onAfterError: (error)-> error.source = "after hook"; @registerError error
159
+ runAfterGroup: -> @runGroup @afterBlocks, ((e)=> @onAfterError e), (=> @onAfterHooks())
160
+ onAfterHooks: -> @onFinish @error
161
+
162
+ class ExampleWithHooks extends AroundBlock
163
+ constructor: (@description, @beforeBlocks, @afterBlocks, @block)->
164
+ runMainBlock: (block, onFinish)-> new Example(block).run @assertions, @context, onFinish
165
+ onAfterHooks: ->
166
+ @handleResult()
167
+ super
168
+
169
+ handleResult: ->
170
+ (@runner.emit 'test:success', name: @description; return) unless @error
171
+ @error.handled = true
172
+ if @error.name is 'AssertionError'
173
+ @runner.emit 'test:failure', name: @description, error: @error
174
+ return
175
+
176
+ if @error.timeout
177
+ @error.source or= 'example'
178
+ @runner.emit 'test:timeout', name: @description, error: @error
179
+ return
180
+ @error.name = 'Exception'
181
+ @error.name += " in #{@error.source}" if @error.source
182
+ @runner.emit 'test:error', name: @description, error: @error
183
+
184
+ class ExampleGroupWithoutHooks
185
+ constructor: (@assertions, @context, @blocks, @onFinish, @onError)-> @nextIndex = 0
186
+
187
+ run: ->
188
+ @wasSuccessful = true
189
+ setTimeout @nextTick, 0
190
+
191
+ nextTick: =>
192
+ (@onFinish(@wasSuccessful); return) unless @nextIndex < @blocks.length
193
+ block = @blocks[@nextIndex++]
194
+ new Example(block).run @assertions, @context, (error)=>
195
+ (@wasSuccessful = false; @onError error) if error
196
+ setTimeout @nextTick, 0
197
+
198
+ class Example
199
+ TICK = 10 # ms
200
+ constructor: (@exampleBlock)->
201
+
202
+ run: (@assertions, @context, @onFinish)->
203
+ @dsl = new ExampleDsl(@assertions.assert, @assertions.expect, @assertions.fail, \
204
+ @assertions.refute)
205
+ if @context
206
+ throw "runs and waitsFor are reserved attributes" if @context.runs or @context.waitsFor
207
+ @context.runs = @dsl.runs
208
+ @context.waitsFor = @dsl.waitsFor
209
+ @tryBlock @exampleBlock, ->
210
+ if @context
211
+ delete @context.runs
212
+ delete @context.waitsFor
213
+ (@onFinish(); return) unless (@steps = @dsl._asyncQueue_).length
214
+ @runNextAsyncStep()
215
+
216
+ tryBlock: (block, onSuccess)->
217
+ try
218
+ context = @context or @dsl
219
+ onSuccess.call this, block.call context, context
220
+ catch error
221
+ error = new Error(error) if typeof error is 'string'
222
+ error.message = "'#{error.message}' in '#{block.description}'" if block.description
223
+ @onFinish error
224
+
225
+ runNextAsyncStep: ->
226
+ (@onFinish(); return) unless @steps.length
227
+ step = @steps.shift()
228
+ if step instanceof Function
229
+ @tryBlock step, @runNextAsyncStep
230
+ else
231
+ @waitsFor step...
232
+
233
+ waitsFor: (@condition, timeout=1000, @description)->
234
+ @deadline = timeout + new Date().getTime()
235
+ @keepTryingCondition()
236
+
237
+ keepTryingCondition: =>
238
+ @tryBlock @condition, (result)->
239
+ (@runNextAsyncStep(); return) if result
240
+ (@onFinish {timeout: true, @description}; return) if new Date().getTime() > @deadline
241
+ setTimeout @keepTryingCondition, TICK
242
+
243
+ class ExampleDsl
244
+ constructor: (@assert, @expect, @fail, @refute)-> @_asyncQueue_ = []
245
+
246
+ runs: (step)=> @_asyncQueue_.push step
247
+
248
+ waitsFor: =>
249
+ for a in arguments
250
+ (condition = a; continue) if typeof a is "function"
251
+ (timeout = a; continue) if typeof a is "number"
252
+ (description = a; continue) if typeof a is "string"
253
+ @_asyncQueue_.push [condition, timeout, description]
254
+
255
+ class StepContext
256
+ constructor: (@assert, @expect, @fail)->
@@ -0,0 +1,5 @@
1
+ /* =require buster/buster-test */
2
+ /* override background image for supporting IE7 */
3
+ .buster-test h1 .buster-logo {
4
+ background-image: url(<%= asset_path "buster/logo.png" %>);
5
+ }
@@ -0,0 +1,15 @@
1
+ module Oojspec
2
+ class OojspecFilter
3
+ def self.filter(controller)
4
+ controller.template = 'oojspec/runner' if controller.params[:path].try :start_with?, 'oojspec'
5
+ end
6
+ end
7
+
8
+ class Engine < ::Rails::Engine
9
+ initializer 'sandbox_assets.oojspec' do |app|
10
+ unless app.config.sandbox_assets.template == 'oojspec/runner'
11
+ SandboxAssets::BaseController.prepend_before_filter OojspecFilter
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Oojspec
2
+ VERSION = "0.0.1"
3
+ end
data/lib/oojspec.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "oojspec/engine"
2
+ require "rails-sandbox-assets"
3
+
4
+ module Oojspec
5
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :oojspec do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,3 @@
1
+ # =require ./buster-core
2
+ # =require ./buster-event-emitter
3
+ # =require_tree .