xspec 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c981df6d76e0ee48d3cea0ebbe598d88e173c6ce
4
+ data.tar.gz: 632dd6c9e7230c07d1edd1d4eac07127ae807b8b
5
+ SHA512:
6
+ metadata.gz: 1c344141ce89c73cb27b7a7f365368f3dc79329f2d06e404397033a277bbff0fd7d8608d5d4f30e31011dc3cbface6917e7c4bf9e17d8f7d915d5700561455f8
7
+ data.tar.gz: 2dd80e45854d14ea0ff1247469d27fda60fc73d0e07b8db7c60e2461d483496cb28f7d06d386a968cddfc673efba5a78e02e2471b7fc33a6152df75cfe05b1ee
@@ -0,0 +1,141 @@
1
+ XSpec
2
+ =====
3
+
4
+ XSpec is an rspec-inspired testing library that is written in a literate style
5
+ and designed to be obvious to use, highly modular, and easy to extend.
6
+
7
+ Open up `lib/xspec.rb` and start reading, or use this [nicely formatted
8
+ version](http://xaviershay.github.io/xspec/).
9
+
10
+ Usage
11
+ -----
12
+
13
+ The default configuration XSpec provides a number of interesting features:
14
+ assertions, doubles, and rich output.
15
+
16
+ ``` ruby
17
+ require 'xspec'
18
+
19
+ extend XSpec.dsl # Use defaults
20
+
21
+ describe 'my application' do
22
+ it 'does math' do
23
+ double = instance_double('Calculator')
24
+ expect(double).add(1, 1) { 2 }
25
+
26
+ assert_equal 2, double.add(1, 1)
27
+ end
28
+
29
+ it 'is slow sometimes' do
30
+ sleep 0.01
31
+ end
32
+
33
+ it 'fails' do
34
+ assert_include "fruit", "punch"
35
+ end
36
+ end
37
+ ```
38
+
39
+ Running this with the built-in runner generates some pretty output. You can't
40
+ see the colors in this README, but trust me they are quite lovely.
41
+
42
+ ```
43
+ > bin/xspec example.rb
44
+
45
+ my application
46
+ 0.000s does math
47
+ 0.011s is slow sometimes
48
+ 0.000s fails - FAILED
49
+
50
+ Timings:
51
+ 0.001 #################### 2
52
+ 0.005 0
53
+ 0.01 0
54
+ 0.1 ########## 1
55
+
56
+
57
+ my application fails:
58
+ "fruit" not present in: "punch"
59
+
60
+ example.rb:18:in `block (2 levels) in <top (required)>'
61
+ bin/xspec:19:in `<main>'
62
+ ```
63
+
64
+ Customization
65
+ -------------
66
+
67
+ Every aspect of XSpec is customizable, from how tests are scheduled and run all
68
+ the way through to formatting of output.
69
+
70
+ Say you wanted boring output with no support for doubles and RSpec
71
+ expectations. You could do that:
72
+
73
+ ``` ruby
74
+ require 'xspec'
75
+
76
+ extend XSpec.dsl(
77
+ assertion_context: XSpec::AssertionContext.stack {
78
+ include XSpec::AssertionContext::RSpecExpectations
79
+ },
80
+ notifier: XSpec::Notifiers::Character.new +
81
+ XSpec::Notifiers::FailuresAtEnd.new
82
+ )
83
+
84
+ describe '...' do
85
+ # etc etc
86
+ end
87
+ ```
88
+
89
+ Of course, you can easily make your own extension classes as well. A runner
90
+ that randomly drops tests and reports random durations? Whatever floats your
91
+ boat:
92
+
93
+ ``` ruby
94
+ require 'xspec'
95
+
96
+ class UnhelpfulRunner
97
+ attr_reader :notifier
98
+
99
+ def initialize(opts)
100
+ @notifier = opts.fetch(:notifier)
101
+ end
102
+
103
+ def run(context)
104
+ notifier.run_start
105
+
106
+ context.nested_units_of_work.each do |x|
107
+ next if rand > 0.9
108
+
109
+ notifier.evaluate_start(x)
110
+
111
+ errors = x.immediate_parent.execute(x)
112
+ duration = rand
113
+ result = XSpec::ExecutedUnitOfWork.new(x, errors, duration)
114
+
115
+ notifier.evaluate_finish(result)
116
+ end
117
+
118
+ notifier.run_finish
119
+ end
120
+ end
121
+
122
+ extend XSpec.dsl(
123
+ evaluator: UnhelpfulRunner.new(notifier: XSpec::Notifier::DEFAULT)
124
+ )
125
+
126
+ describe '...' do
127
+ # etc etc
128
+ end
129
+ ```
130
+
131
+ Developing
132
+ ----------
133
+
134
+ Follow the idioms you find in the source, they are somewhat different than
135
+ a traditional Ruby project. Bug fixes welcome, features likely to be rejected
136
+ since I have a strong opinion of what this library should and should not do.
137
+ Talk to me before embarking on anything large. Tests are written in XSpec,
138
+ which might do your head in:
139
+
140
+ bundle install
141
+ bundle exec bin/run-specs
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift "spec"
4
+ $LOAD_PATH.unshift "lib"
5
+
6
+ files = if ARGV.any? {|x| x.length > 0 }
7
+ ARGV
8
+ else
9
+ Dir['spec/**/*_spec.rb']
10
+ end
11
+
12
+ require 'xspec'
13
+
14
+ files.each do |f|
15
+ load f
16
+ end
17
+
18
+ if respond_to?(:run!)
19
+ exit 1 unless run!
20
+ else
21
+ $stderr.puts "This script can only be used when XSpec.dsl is mixed in to " +
22
+ "global scope."
23
+ exit 1
24
+ end
@@ -0,0 +1,61 @@
1
+ # # XSpec
2
+
3
+ # Hello and welcome to XSpec!
4
+ #
5
+ # XSpec is an rspec-inspired testing library that is designed to be highly
6
+ # modular and easy to extend. Let's dive right in.
7
+ module XSpec
8
+ # The DSL is the core of XSpec. It dynamically generates a module that can be
9
+ # mixed into which ever context you choose (using `extend`), be that the
10
+ # top-level namespace or a specific class.
11
+ #
12
+ # This enables different options to be specified per DSL, which is at the
13
+ # heart of XSpec's modularity. It is easy to change every component to your
14
+ # liking.
15
+ def dsl(options = {})
16
+ options = XSpec.add_defaults(options)
17
+
18
+ Module.new do
19
+ include DSL
20
+
21
+ # Each DSL has its own independent context, which is described in detail
22
+ # in `data_structures.rb`.
23
+ def __xspec_context
24
+ assertion_context = __xspec_opts.fetch(:assertion_context)
25
+ @__xspec_context ||= XSpec::Context.root(assertion_context)
26
+ end
27
+
28
+ # Some meta-magic is needed to enable the options from local scope above
29
+ # to be available inside the module.
30
+ define_method(:__xspec_opts) { options }
31
+
32
+ # `run!` is where the magic happens. Typically called at the end of a
33
+ # file (or by `autorun!`), this method takes all the data that was
34
+ # accumulated by the DSL methods above and runs it through the evaluator.
35
+ def run!
36
+ __xspec_opts.fetch(:evaluator).run(__xspec_context)
37
+ end
38
+
39
+ # It is often convenient to trigger a run after all files have been
40
+ # processed, which is what `autorun!` sets up for you. Requiring
41
+ # `xspec/autorun` does this automatically for you.
42
+ def autorun!
43
+ at_exit do
44
+ exit 1 unless run!
45
+ end
46
+ end
47
+ end
48
+ end
49
+ module_function :dsl
50
+ end
51
+
52
+ # Understanding the data structures used by XSpec will assist you in
53
+ # understanding the behavoural components such as the evaluator and notifier.
54
+ require 'xspec/data_structures'
55
+
56
+ # To further explore the code base, dive into the defaults file, which
57
+ # describes the different sub-components of XSpec that you can use or
58
+ # customize.
59
+ require 'xspec/defaults'
60
+
61
+ require 'xspec/dsl'
@@ -0,0 +1,360 @@
1
+ # # Assertion Contexts
2
+
3
+ # Assertion contexts are composed together into a context stack. The final
4
+ # stack has a single API method `call`, which is sent the unit of work to be
5
+ # executed and must return an array of `Failure` objects. It should not allow
6
+ # code-level exceptions to be raised, though should not block system exceptions
7
+ # (`SignalException`, `NoMemoryError`, etc).
8
+ module XSpec
9
+ module AssertionContext
10
+ # A stack is typically book-ended by the top and bottom contexts, so this
11
+ # helper is the most commond way to build up a custom stack.
12
+ def self.stack(&block)
13
+ Module.new do
14
+ include Bottom
15
+ instance_exec &block
16
+ include Top
17
+ end
18
+ end
19
+
20
+ # The bottom context executes the unit of work, with no error handling or
21
+ # extra behaviour. By separating this, all other contexts layered on top of
22
+ # this one can just call `super`, making them easy to compose.
23
+ module Bottom
24
+ def call(unit_of_work)
25
+ instance_exec(&unit_of_work.block)
26
+ []
27
+ end
28
+ end
29
+
30
+ # The top should be included as the final module in a context stack. It is
31
+ # a catch all to make sure all standard exceptions have been handled and
32
+ # do not leak outside the stack.
33
+ module Top
34
+ def call(unit_of_work)
35
+ super
36
+ rescue => e
37
+ [CodeException.new(unit_of_work, e.message, e.backtrace)]
38
+ end
39
+ end
40
+
41
+ # ### Simple Assertions
42
+ #
43
+ # This simple context provides very straight-forward assertion methods.
44
+ module Simple
45
+ class AssertionFailed < RuntimeError
46
+ attr_reader :message, :backtrace
47
+
48
+ def initialize(message, backtrace)
49
+ @message = message
50
+ @backtrace = backtrace
51
+ end
52
+ end
53
+
54
+ def call(unit_of_work)
55
+ super
56
+ rescue AssertionFailed => e
57
+ [Failure.new(unit_of_work, e.message, e.backtrace)]
58
+ end
59
+
60
+ def assert(proposition, message=nil)
61
+ unless proposition
62
+ message ||= 'assertion failed'
63
+
64
+ _raise message
65
+ end
66
+ end
67
+
68
+ def assert_equal(expected, actual)
69
+ unless expected == actual
70
+ message ||= <<-EOS.chomp
71
+ want: #{expected.inspect}
72
+ got: #{actual.inspect}
73
+ EOS
74
+
75
+ _raise message
76
+ end
77
+ end
78
+
79
+ def assert_include(expected, output)
80
+ assert output.include?(expected),
81
+ "#{expected.inspect} not present in: #{output.inspect}"
82
+ end
83
+
84
+ def fail(message = nil)
85
+ message ||= 'failed'
86
+
87
+ _raise message
88
+ end
89
+
90
+ private
91
+
92
+ def _raise(message)
93
+ raise AssertionFailed.new(message, caller)
94
+ end
95
+ end
96
+
97
+ # ### Doubles
98
+ #
99
+ # The doubles module provides test doubles that can be used in-place of
100
+ # real objects.
101
+ module Doubles
102
+ DoubleFailure = Class.new(RuntimeError)
103
+
104
+ def call(unit_of_work)
105
+ super
106
+ rescue DoubleFailure => e
107
+ [Failure.new(unit_of_work, e.message, e.backtrace)]
108
+ end
109
+
110
+ # It can be configured with a few options:
111
+ #
112
+ # * `auto_verify` calls `assert_exhausted` on all created doubles after a
113
+ # unit of work executes successfully to ensure that all expectations that
114
+ # were set were actually called.
115
+ # * `strict` forbids doubling of classes that have not been loaded. This
116
+ # should generally be enabled when doing a full spec run, and disabled
117
+ # when running specs in isolation.
118
+ #
119
+ # The `with` method returns a module that can be included in a stack.
120
+ def self.with(*opts)
121
+ modules = [self] + opts.map {|x| {
122
+ auto_verify: AutoVerify,
123
+ strict: Strict
124
+ }.fetch(x) }
125
+
126
+
127
+ Module.new do
128
+ modules.each do |m|
129
+ include m
130
+ end
131
+ end
132
+ end
133
+
134
+ # An instance double stands in for an instance of the given class
135
+ # reference, given as a string. The class does not need to be loaded, but
136
+ # if it is then only public instance methods defined on the class are
137
+ # able to be expected.
138
+ def instance_double(klass)
139
+ _double(klass, InstanceReference)
140
+ end
141
+
142
+ # Simarly, a class double validates that class responds to all expected
143
+ # methods, if that class has been loaded.
144
+ def class_double(klass)
145
+ _double(klass, ClassReference)
146
+ end
147
+
148
+ # If the doubled class has not been loaded, a null object reference is
149
+ # used that allows expecting of all methods.
150
+ def _double(klass, type)
151
+ ref = if self.class.const_defined?(klass)
152
+ type.new(self.class.const_get(klass))
153
+ else
154
+ StringReference.new(klass)
155
+ end
156
+
157
+ Double.new(ref)
158
+ end
159
+
160
+ # To set up an expectation on a double, call the expected method an
161
+ # arguments on the proxy object returned by `expect`. If a return value
162
+ # is desired, it can be supplied as a block, for example:
163
+ # `expect(double).some_method(1, 2) { "return value" }`
164
+ def expect(obj)
165
+ Recorder.new(obj)
166
+ end
167
+
168
+ class Recorder
169
+ def initialize(double)
170
+ @double = double
171
+ end
172
+
173
+ def method_missing(*args, &ret)
174
+ @double._expect(args, &(ret || ->{}))
175
+ end
176
+ end
177
+
178
+ # Since the double object inherits from `BasicObject`, virtually every
179
+ # method call will be routed through `method_missing`. From there, the
180
+ # call can be checked against the expectations that were setup at the
181
+ # beginning of a spec.
182
+ class Double < BasicObject
183
+ def initialize(klass)
184
+ @klass = klass
185
+ @expected = []
186
+ end
187
+
188
+ def method_missing(*actual_args)
189
+ i = @expected.find_index {|expected_args, ret|
190
+ expected_args == actual_args
191
+ }
192
+
193
+ if i
194
+ @expected.delete_at(i)[1].call
195
+ else
196
+ name, rest = *actual_args
197
+ ::Kernel.raise DoubleFailure, "Unexpectedly received: %s(%s)" % [
198
+ name,
199
+ [*rest].map(&:inspect).join(", ")
200
+ ]
201
+ end
202
+ end
203
+
204
+ # The two methods needed on this object to set it up and verify it are
205
+ # prefixed by `_` to try to ensure they don't clash with any method
206
+ # expectations. While not fail-safe, users should only be using
207
+ # expectations for a public API, and `_` is traditionally only used
208
+ # for private methods (if at all).
209
+ def _expect(args, &ret)
210
+ @klass.validate_call! args
211
+
212
+ @expected << [args, ret]
213
+ end
214
+
215
+ def _verify
216
+ return if @expected.empty?
217
+
218
+ ::Kernel.raise DoubleFailure, "%s double did not receive:\n%s" % [
219
+ @klass.to_s,
220
+ @expected.map {|(name, *args), _|
221
+ " %s(%s)" % [name, args.map(&:inspect).join(", ")]
222
+ }.join("\n")
223
+ ]
224
+ end
225
+ end
226
+
227
+ # A reference can be thought of as a "backing object" for a double. It
228
+ # provides an API to validate that a method being expected actually
229
+ # exists - the implementation is different for the different types of
230
+ # doubles.
231
+ class Reference
232
+ def initialize(klass)
233
+ @klass = klass
234
+ end
235
+
236
+ def validate_call!(args)
237
+ end
238
+
239
+ def to_s
240
+ @klass.to_s
241
+ end
242
+ end
243
+
244
+ # A string reference is the "null object" of references, allowing all
245
+ # methods to be expected. It is used when nothing is known about the
246
+ # referenced class (such as when it has not been loaded).
247
+ class StringReference < Reference
248
+ end
249
+
250
+ # Class and Instance references are backed by loaded classes, and
251
+ # restrict the messages that can be expected on a double.
252
+ class ClassReference < Reference
253
+ def validate_call!(args)
254
+ name, rest = *args
255
+
256
+ unless @klass.respond_to?(name)
257
+ raise DoubleFailure,
258
+ "#{@klass}.#{name} is unimplemented or not public"
259
+ end
260
+ end
261
+ end
262
+
263
+ class InstanceReference < Reference
264
+ def validate_call!(args)
265
+ name, rest = *args
266
+
267
+ unless @klass.public_instance_methods.include?(name)
268
+ raise DoubleFailure,
269
+ "#{@klass}##{name} is unimplemented or not public"
270
+ end
271
+ end
272
+ end
273
+
274
+ # The `:strict` option mixes in this `Strict` module, which raises rather
275
+ # than create `StringReference`s for unknown classes.
276
+ module Strict
277
+ def _double(klass, type)
278
+ ref = if self.class.const_defined?(klass)
279
+ type.new(self.class.const_get(klass))
280
+ else
281
+ raise DoubleFailure, "#{klass} is not a valid class name"
282
+ end
283
+
284
+ super
285
+ end
286
+ end
287
+
288
+ # An assertion is provided to validate that all expected methods were
289
+ # called on a double.
290
+ def assert_exhausted(obj)
291
+ obj._verify
292
+ end
293
+
294
+ # Most of the time, `assert_exhausted` will not be called directly, since
295
+ # the `:auto_verify` option can be used to call it by default on all
296
+ # doubles. That option mixes in this `AutoVerify` module to augment
297
+ # methods necessary for this behaviour.
298
+ module AutoVerify
299
+ def initialize
300
+ @doubles = []
301
+ end
302
+
303
+ def call(unit_of_work)
304
+ result = super
305
+
306
+ if result.empty?
307
+ @doubles.each do |double|
308
+ assert_exhausted double
309
+ end
310
+ end
311
+
312
+ result
313
+ rescue DoubleFailure => e
314
+ [Failure.new(unit_of_work, e.message, e.backtrace)]
315
+ end
316
+
317
+ def class_double(klass)
318
+ x = super
319
+ @doubles << x
320
+ x
321
+ end
322
+
323
+ def instance_double(klass)
324
+ x = super
325
+ @doubles << x
326
+ x
327
+ end
328
+ end
329
+ end
330
+
331
+ # ### RSpec Integration
332
+ #
333
+ # This RSpec adapter shows two useful techniques: dynamic library loading
334
+ # which removes RSpec as a direct dependency, and use of the `mixin`
335
+ # method to further extend the target context.
336
+ module RSpecExpectations
337
+ def self.included(context)
338
+ begin
339
+ require 'rspec/expectations'
340
+ require 'rspec/matchers'
341
+ rescue LoadError
342
+ raise "RSpec is not available, cannot use RSpec assertion context."
343
+ end
344
+
345
+ context.include(RSpec::Matchers)
346
+ end
347
+
348
+ def call(unit_of_work)
349
+ super
350
+ rescue RSpec::Expectations::ExpectationNotMetError => e
351
+ [Failure.new(unit_of_work, e.message, e.backtrace)]
352
+ end
353
+ end
354
+
355
+ DEFAULT = stack do
356
+ include Simple
357
+ include Doubles.with(:auto_verify)
358
+ end
359
+ end
360
+ end