xspec 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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