xspec 0.0.2 → 0.1.0
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.
- checksums.yaml +4 -4
- data/README.md +22 -50
- data/bin/xspec +4 -2
- data/lib/xspec.rb +18 -11
- data/lib/xspec/data_structures.rb +15 -15
- data/lib/xspec/defaults.rb +6 -6
- data/lib/xspec/dsl.rb +5 -0
- data/lib/xspec/evaluators.rb +316 -22
- data/lib/xspec/notifiers.rb +2 -0
- data/lib/xspec/schedulers.rb +39 -0
- data/spec/integration/rspec_expectations_spec.rb +3 -3
- data/spec/spec_helper.rb +0 -2
- data/spec/unit/assertion_spec.rb +7 -7
- data/spec/unit/doubles_spec.rb +93 -115
- data/xspec.gemspec +2 -2
- metadata +4 -4
- data/lib/xspec/assertion_contexts.rb +0 -382
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5dba5d7629f6d87591f9abe883290f50372dc64
|
4
|
+
data.tar.gz: ead15780cdec15272984c1580051e7d7f2eadded
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b37433a77450176d0cf1ebeae40d5ad24ef28ce8095887126edfd5dfd0040fdcfbf418a1726862a60eb7f275f347d19a371720331b5376145e256bff08374ef5
|
7
|
+
data.tar.gz: 963b1b472454aeaeb03aa956a1e38ff3a9b1a9bb4bf78fcd8c9eb06aae26a31d4591ed8e7b9a4fa1c11f43a7413a2d37112d2bd140c182145fcadc8745922b91
|
data/README.md
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
XSpec
|
2
2
|
=====
|
3
3
|
|
4
|
-
XSpec is an rspec-inspired testing library that is written in a
|
5
|
-
and designed to be obvious to use, highly modular, and easy to
|
6
|
-
|
7
|
-
Open up `lib/xspec.rb` and start reading, or use this [nicely formatted
|
8
|
-
version](http://xaviershay.github.io/xspec/).
|
4
|
+
XSpec is an rspec-inspired testing library for Ruby that is written in a
|
5
|
+
literate style and designed to be obvious to use, highly modular, and easy to
|
6
|
+
extend.
|
9
7
|
|
10
8
|
Usage
|
11
9
|
-----
|
12
10
|
|
11
|
+
gem install xspec
|
12
|
+
|
13
13
|
The default configuration XSpec provides a number of interesting features:
|
14
14
|
assertions, doubles, and rich output.
|
15
15
|
|
@@ -40,7 +40,7 @@ Running this with the built-in runner generates some pretty output. You can't
|
|
40
40
|
see the colors in this README, but trust me they are quite lovely.
|
41
41
|
|
42
42
|
```
|
43
|
-
>
|
43
|
+
> xspec example.rb
|
44
44
|
|
45
45
|
my application
|
46
46
|
0.000s does math
|
@@ -61,8 +61,7 @@ my application fails:
|
|
61
61
|
bin/xspec:19:in `<main>'
|
62
62
|
```
|
63
63
|
|
64
|
-
Customization
|
65
|
-
-------------
|
64
|
+
### Customization
|
66
65
|
|
67
66
|
Every aspect of XSpec is customizable, from how tests are scheduled and run all
|
68
67
|
the way through to formatting of output.
|
@@ -86,56 +85,29 @@ describe '...' do
|
|
86
85
|
end
|
87
86
|
```
|
88
87
|
|
89
|
-
Of course, you can
|
90
|
-
|
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
|
88
|
+
Of course, you can make your own extension classes as well. For details, see
|
89
|
+
the "Configuration" section of the documentation.
|
108
90
|
|
109
|
-
|
110
|
-
|
111
|
-
errors = x.immediate_parent.execute(x)
|
112
|
-
duration = rand
|
113
|
-
result = XSpec::ExecutedUnitOfWork.new(x, errors, duration)
|
91
|
+
Documentation
|
92
|
+
-------------
|
114
93
|
|
115
|
-
|
116
|
-
end
|
94
|
+
There are two major sources of documentation:
|
117
95
|
|
118
|
-
|
119
|
-
|
120
|
-
end
|
96
|
+
* [Main API documentation.](https://xaviershay.github.io/xspec/api.html)
|
97
|
+
* [Literate source code.](https://xaviershay.github.io/xspec/)
|
121
98
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
describe '...' do
|
127
|
-
# etc etc
|
128
|
-
end
|
129
|
-
```
|
99
|
+
It is expected that regular users of XSpec will read both at least once. There
|
100
|
+
isn't much to them, and they will give you a useful mental model of how XSpec
|
101
|
+
works.
|
130
102
|
|
131
103
|
Developing
|
132
104
|
----------
|
133
105
|
|
134
106
|
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
|
136
|
-
since I have a strong opinion of what this library should and should
|
137
|
-
Talk to me before embarking on anything large. Tests are written in
|
138
|
-
which might do your head in:
|
107
|
+
a traditional Ruby project. Bug fixes welcome, though features are likely to be
|
108
|
+
rejected since I have a strong opinion of what this library should and should
|
109
|
+
not do. Talk to me before embarking on anything large. Tests are written in
|
110
|
+
XSpec, which might do your head in:
|
139
111
|
|
140
112
|
bundle install
|
141
|
-
bin/
|
113
|
+
bundle exec bin/xspec
|
data/bin/xspec
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
|
4
|
+
|
5
|
+
require 'xspec'
|
6
|
+
|
3
7
|
$LOAD_PATH.unshift "spec"
|
4
8
|
$LOAD_PATH.unshift "lib"
|
5
9
|
|
@@ -9,8 +13,6 @@ else
|
|
9
13
|
Dir['spec/**/*_spec.rb']
|
10
14
|
end
|
11
15
|
|
12
|
-
require 'xspec'
|
13
|
-
|
14
16
|
files.each do |f|
|
15
17
|
load f
|
16
18
|
end
|
data/lib/xspec.rb
CHANGED
@@ -16,13 +16,16 @@ module XSpec
|
|
16
16
|
options = XSpec.add_defaults(options)
|
17
17
|
|
18
18
|
Module.new do
|
19
|
+
# Each DSL provides a standard set of methods provided by the [DSL
|
20
|
+
# module](dsl.html).
|
19
21
|
include DSL
|
20
22
|
|
21
|
-
#
|
22
|
-
# in
|
23
|
+
# In addition, each DSL has its own independent context, which is
|
24
|
+
# described in detail in the
|
25
|
+
# [`data_structures.rb`](data_structures.html).
|
23
26
|
def __xspec_context
|
24
|
-
|
25
|
-
@__xspec_context ||= XSpec::Context.root(
|
27
|
+
evaluator = __xspec_opts.fetch(:evaluator)
|
28
|
+
@__xspec_context ||= XSpec::Context.root(evaluator)
|
26
29
|
end
|
27
30
|
|
28
31
|
# Some meta-magic is needed to enable the options from local scope above
|
@@ -31,9 +34,12 @@ module XSpec
|
|
31
34
|
|
32
35
|
# `run!` is where the magic happens. Typically called at the end of a
|
33
36
|
# file (or by `autorun!`), this method takes all the data that was
|
34
|
-
# accumulated by the DSL methods above and runs it through the
|
37
|
+
# accumulated by the DSL methods above and runs it through the scheduler.
|
35
38
|
def run!
|
36
|
-
__xspec_opts.fetch(:
|
39
|
+
notifier = __xspec_opts.fetch(:notifier)
|
40
|
+
scheduler = __xspec_opts.fetch(:scheduler)
|
41
|
+
|
42
|
+
scheduler.run(__xspec_context, notifier)
|
37
43
|
end
|
38
44
|
|
39
45
|
# It is often convenient to trigger a run after all files have been
|
@@ -49,13 +55,14 @@ module XSpec
|
|
49
55
|
module_function :dsl
|
50
56
|
end
|
51
57
|
|
52
|
-
# Understanding the data structures used by XSpec will
|
53
|
-
# understanding the behavoural components such as the
|
58
|
+
# Understanding the [data structures](data_structures.html) used by XSpec will
|
59
|
+
# assist you in understanding the behavoural components such as the scheduler
|
60
|
+
# and notifier. Read it next.
|
54
61
|
require 'xspec/data_structures'
|
55
62
|
|
56
|
-
# To further explore the code base, dive into the defaults
|
57
|
-
# describes the different sub-components of XSpec
|
58
|
-
# customize.
|
63
|
+
# To further explore the code base, dive into the [defaults
|
64
|
+
# file](defaults.html), which describes the different sub-components of XSpec
|
65
|
+
# that you can use or customize.
|
59
66
|
require 'xspec/defaults'
|
60
67
|
|
61
68
|
require 'xspec/dsl'
|
@@ -7,7 +7,7 @@
|
|
7
7
|
module XSpec
|
8
8
|
# A unit of work, usually created by the `it` DSL method, is a labeled,
|
9
9
|
# indivisible code block that expresses an assertion about a property of the
|
10
|
-
# system under test. They are run by
|
10
|
+
# system under test. They are run by a scheduler.
|
11
11
|
UnitOfWork = Struct.new(:name, :block)
|
12
12
|
|
13
13
|
|
@@ -20,7 +20,7 @@ module XSpec
|
|
20
20
|
require 'xspec/dsl'
|
21
21
|
class Context
|
22
22
|
class << self
|
23
|
-
attr_reader :name, :children, :units_of_work, :
|
23
|
+
attr_reader :name, :children, :units_of_work, :evaluator
|
24
24
|
|
25
25
|
# A context includes the same DSL methods as the root level module, which
|
26
26
|
# enables the recursive creation.
|
@@ -29,33 +29,33 @@ module XSpec
|
|
29
29
|
|
30
30
|
# Each nested context creates a new class that inherits from the parent.
|
31
31
|
# Methods can be added to this class as per normal, and are correctly
|
32
|
-
# inherited by children. When it comes time to run tests, the
|
32
|
+
# inherited by children. When it comes time to run tests, the scheduler
|
33
33
|
# will create a new instance of the context (a class) for each test,
|
34
34
|
# making the defined methods available and also ensuring that there is no
|
35
35
|
# state pollution between tests.
|
36
|
-
def make(name,
|
36
|
+
def make(name, evaluator, &block)
|
37
37
|
x = Class.new(self)
|
38
|
-
x.initialize!(name,
|
38
|
+
x.initialize!(name, evaluator)
|
39
39
|
x.class_eval(&block) if block
|
40
|
-
x.
|
40
|
+
x.apply_evaluator!
|
41
41
|
x
|
42
42
|
end
|
43
43
|
|
44
44
|
# A class cannot have an implicit initializer, but some variable
|
45
45
|
# inititialization is required so the `initialize!` method is called
|
46
46
|
# explicitly when ever a dynamic subclass is created.
|
47
|
-
def initialize!(name,
|
47
|
+
def initialize!(name, evaluator)
|
48
48
|
@children = []
|
49
49
|
@units_of_work = []
|
50
50
|
@name = name
|
51
|
-
@
|
51
|
+
@evaluator = evaluator
|
52
52
|
end
|
53
53
|
|
54
54
|
# The assertion context should be applied after the user has had a chance
|
55
55
|
# to add their own methods. It needs to be last so that users can't
|
56
56
|
# clobber the assertion methods.
|
57
|
-
def
|
58
|
-
include(
|
57
|
+
def apply_evaluator!
|
58
|
+
include(evaluator)
|
59
59
|
end
|
60
60
|
|
61
61
|
# Executing a unit of work creates a new instance and hands it off to the
|
@@ -68,14 +68,14 @@ module XSpec
|
|
68
68
|
|
69
69
|
# The root context is nothing special, and behaves the same as all the
|
70
70
|
# others.
|
71
|
-
def root(
|
72
|
-
make(nil,
|
71
|
+
def root(evaluator)
|
72
|
+
make(nil, evaluator)
|
73
73
|
end
|
74
74
|
|
75
75
|
# Child contexts and units of work are typically added by the `describe`
|
76
76
|
# and `it` DSL methods respectively.
|
77
77
|
def add_child_context(name = nil, opts = {}, &block)
|
78
|
-
self.children << make(name,
|
78
|
+
self.children << make(name, evaluator, &block)
|
79
79
|
end
|
80
80
|
|
81
81
|
def add_unit_of_work(name = nil, opts = {}, &block)
|
@@ -91,13 +91,13 @@ module XSpec
|
|
91
91
|
# This is leaky abstraction, since only units of work are copied from
|
92
92
|
# shared contexts. Methods and child contexts are ignored.
|
93
93
|
def create_shared_context(&block)
|
94
|
-
make(nil,
|
94
|
+
make(nil, evaluator, &block)
|
95
95
|
end
|
96
96
|
|
97
97
|
def copy_into_tree(source_context)
|
98
98
|
target_context = make(
|
99
99
|
source_context.name,
|
100
|
-
source_context.
|
100
|
+
source_context.evaluator
|
101
101
|
)
|
102
102
|
source_context.nested_units_of_work.each do |x|
|
103
103
|
target_context.units_of_work << x.unit_of_work
|
data/lib/xspec/defaults.rb
CHANGED
@@ -2,9 +2,9 @@
|
|
2
2
|
|
3
3
|
# These are the defaults used by `XSpec.dsl`, but feel free to specify your own
|
4
4
|
# instead. They are set up in such a way that if you can override a component
|
5
|
-
# down in the bowels without having to provide an entire top level
|
5
|
+
# down in the bowels without having to provide an entire top level scheduler.
|
6
|
+
require 'xspec/schedulers'
|
6
7
|
require 'xspec/evaluators'
|
7
|
-
require 'xspec/assertion_contexts'
|
8
8
|
require 'xspec/notifiers'
|
9
9
|
|
10
10
|
module XSpec
|
@@ -18,12 +18,12 @@ module XSpec
|
|
18
18
|
# This is a module that is included as the final step in constructing a
|
19
19
|
# context. Allows for different matchers and expectation frameworks to be
|
20
20
|
# used.
|
21
|
-
options[:
|
21
|
+
options[:evaluator] ||= Evaluator::DEFAULT
|
22
22
|
|
23
|
-
# An
|
23
|
+
# An scheduler is responsible for scheduling units of work and handing them
|
24
24
|
# off to the assertion context. Any logic regarding threads, remote
|
25
|
-
# execution or the like belongs in
|
26
|
-
options[:
|
25
|
+
# execution or the like belongs in a scheduler.
|
26
|
+
options[:scheduler] ||= Scheduler::DEFAULT
|
27
27
|
options
|
28
28
|
end
|
29
29
|
module_function :add_defaults
|
data/lib/xspec/dsl.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
|
+
# # DSL
|
2
|
+
|
1
3
|
# Common DSL functions are provided as a module so that they can be used in
|
2
4
|
# both top-level and nested contexts. The method names are modeled after
|
3
5
|
# rspec, and should behave roughly the same.
|
6
|
+
#
|
7
|
+
# They delegate to method in the [current context](xspec.html#section-5) named
|
8
|
+
# in a way that more accurately represents XSpec implementation details.
|
4
9
|
module XSpec
|
5
10
|
module DSL
|
6
11
|
def it(*args, &block)
|
data/lib/xspec/evaluators.rb
CHANGED
@@ -1,40 +1,334 @@
|
|
1
1
|
# # Evaluators
|
2
2
|
|
3
|
-
# Evaluators are
|
4
|
-
#
|
3
|
+
# Evaluators are usually composed together into a stack. The final stack has a
|
4
|
+
# single API method `call`, which is sent the unit of work to be executed and
|
5
|
+
# must return an array of `Failure` objects. It should not allow code-level
|
6
|
+
# exceptions to be raised, though should not block system exceptions
|
7
|
+
# (`SignalException`, `NoMemoryError`, etc).
|
5
8
|
module XSpec
|
6
9
|
module Evaluator
|
7
|
-
#
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
# A stack is typically book-ended by the top and bottom evaluators, 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 evaluator executes the unit of work, with no error handling or
|
21
|
+
# extra behaviour. By separating this, all other evaluators layered on top
|
22
|
+
# of 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 usually be included as the final module in a stack. It is
|
31
|
+
# a catch all to make sure all standard exceptions have been handled and do
|
32
|
+
# 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 evaluator 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 the following options:
|
111
|
+
#
|
112
|
+
# * `strict` forbids doubling of classes that have not been loaded. This
|
113
|
+
# should generally be enabled when doing a full spec run, and disabled
|
114
|
+
# when running specs in isolation.
|
115
|
+
#
|
116
|
+
# The `with` method returns a module that can be included in a stack.
|
117
|
+
def self.with(*opts)
|
118
|
+
modules = [self] + opts.map {|x| {
|
119
|
+
strict: Strict
|
120
|
+
}.fetch(x) }
|
121
|
+
|
122
|
+
|
123
|
+
Module.new do
|
124
|
+
modules.each do |m|
|
125
|
+
include m
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# An instance double stands in for an instance of the given class
|
131
|
+
# reference, given as a string. The class does not need to be loaded, but
|
132
|
+
# if it is then only public instance methods defined on the class are
|
133
|
+
# able to be expected.
|
134
|
+
def instance_double(klass)
|
135
|
+
_double(klass, InstanceReference)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Simarly, a class double validates that class responds to all expected
|
139
|
+
# methods, if that class has been loaded.
|
140
|
+
def class_double(klass)
|
141
|
+
_double(klass, ClassReference)
|
142
|
+
end
|
143
|
+
|
144
|
+
# If the doubled class has not been loaded, a null object reference is
|
145
|
+
# used that allows expecting of all methods.
|
146
|
+
def _double(klass, type)
|
147
|
+
ref = if self.class.const_defined?(klass)
|
148
|
+
type.new(self.class.const_get(klass))
|
149
|
+
else
|
150
|
+
StringReference.new(klass)
|
151
|
+
end
|
152
|
+
|
153
|
+
Double.new(ref)
|
14
154
|
end
|
15
155
|
|
16
|
-
|
17
|
-
|
156
|
+
# Use `verify` to assert that a method was called on a double with
|
157
|
+
# particular arguments. Doubles record all received messages, so `verify`
|
158
|
+
# should be called at the end of your test.
|
159
|
+
def verify(obj)
|
160
|
+
Proxy.new(obj, :_verify)
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
# All messages sent to a double will return `nil`. Use `stub` to specify
|
165
|
+
# a return value instead: `stub(double).some_method(1, 2) { "return
|
166
|
+
# value" }`. This must be called before the message is sent to the
|
167
|
+
# double.
|
168
|
+
def stub(obj)
|
169
|
+
Proxy.new(obj, :_stub)
|
170
|
+
end
|
171
|
+
|
172
|
+
# The proxy object captures messages sent to it and passes them through
|
173
|
+
# to either the `_verify` of `_stub` method on the double.
|
174
|
+
class Proxy < BasicObject
|
175
|
+
def initialize(double, method)
|
176
|
+
@double = double
|
177
|
+
@method = method
|
178
|
+
end
|
179
|
+
|
180
|
+
def method_missing(*args, &ret)
|
181
|
+
@double.__send__(@method, args, &(ret || ->{}))
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Since the double object inherits from `BasicObject`, virtually every
|
186
|
+
# method call will be routed through `method_missing`. From there, the
|
187
|
+
# message can be recorded and an appropriate return value selected from
|
188
|
+
# the stubs.
|
189
|
+
class Double < BasicObject
|
190
|
+
def initialize(klass)
|
191
|
+
@klass = klass
|
192
|
+
@expected = []
|
193
|
+
@received = []
|
194
|
+
end
|
195
|
+
|
196
|
+
def method_missing(*actual_args)
|
197
|
+
stub = @expected.find {|expected_args, ret|
|
198
|
+
expected_args == actual_args
|
199
|
+
}
|
200
|
+
|
201
|
+
ret = if stub
|
202
|
+
stub[1].call
|
203
|
+
end
|
204
|
+
|
205
|
+
@received << actual_args
|
206
|
+
|
207
|
+
ret
|
208
|
+
end
|
209
|
+
|
210
|
+
# The two methods needed on this object to set it up and verify it are
|
211
|
+
# prefixed by `_` to try to ensure they don't clash with any method
|
212
|
+
# expectations. While not fail-safe, users should only be using
|
213
|
+
# expectations for a public API, and `_` is traditionally only used
|
214
|
+
# for private methods (if at all).
|
215
|
+
def _stub(args, &ret)
|
216
|
+
@klass.validate_call! args
|
217
|
+
|
218
|
+
@expected << [args, ret]
|
219
|
+
end
|
220
|
+
|
221
|
+
def _verify(args)
|
222
|
+
@klass.validate_call! args
|
223
|
+
|
224
|
+
i = @received.index(args)
|
225
|
+
|
226
|
+
if i
|
227
|
+
@received.delete_at(i)
|
228
|
+
else
|
229
|
+
name, rest = *args
|
230
|
+
::Kernel.raise DoubleFailure,
|
231
|
+
"Did not receive: %s(%s)\nDid receive:%s\n" % [
|
232
|
+
name,
|
233
|
+
[*rest].map(&:inspect).join(", "),
|
234
|
+
@received.map {|name, *args|
|
235
|
+
" %s(%s)" % [name, args.map(&:inspect).join(", ")]
|
236
|
+
}.join("\n")
|
237
|
+
]
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# A reference can be thought of as a "backing object" for a double. It
|
243
|
+
# provides an API to validate that a method being expected actually
|
244
|
+
# exists - the implementation is different for the different types of
|
245
|
+
# doubles.
|
246
|
+
class Reference
|
247
|
+
def initialize(klass)
|
248
|
+
@klass = klass
|
249
|
+
end
|
250
|
+
|
251
|
+
def validate_call!(args)
|
252
|
+
end
|
253
|
+
|
254
|
+
def to_s
|
255
|
+
@klass.to_s
|
256
|
+
end
|
257
|
+
end
|
18
258
|
|
19
|
-
|
20
|
-
|
259
|
+
# A string reference is the "null object" of references, allowing all
|
260
|
+
# methods to be expected. It is used when nothing is known about the
|
261
|
+
# referenced class (such as when it has not been loaded).
|
262
|
+
class StringReference < Reference
|
263
|
+
end
|
21
264
|
|
22
|
-
|
23
|
-
|
24
|
-
|
265
|
+
# Class and Instance references are backed by loaded classes, and
|
266
|
+
# restrict the messages that can be expected on a double.
|
267
|
+
class ClassReference < Reference
|
268
|
+
def validate_call!(args)
|
269
|
+
name, rest = *args
|
25
270
|
|
26
|
-
|
27
|
-
|
271
|
+
unless @klass.respond_to?(name)
|
272
|
+
raise DoubleFailure,
|
273
|
+
"#{@klass}.#{name} is unimplemented or not public"
|
274
|
+
end
|
28
275
|
end
|
276
|
+
end
|
277
|
+
|
278
|
+
class InstanceReference < Reference
|
279
|
+
def validate_call!(args)
|
280
|
+
name, rest = *args
|
29
281
|
|
30
|
-
|
282
|
+
unless @klass.public_instance_methods.include?(name)
|
283
|
+
raise DoubleFailure,
|
284
|
+
"#{@klass}##{name} is unimplemented or not public"
|
285
|
+
end
|
286
|
+
end
|
31
287
|
end
|
32
288
|
|
33
|
-
|
289
|
+
# The `:strict` option mixes in this `Strict` module, which raises rather
|
290
|
+
# than create a `StringReference` for unknown classes.
|
291
|
+
module Strict
|
292
|
+
def _double(klass, type)
|
293
|
+
ref = if self.class.const_defined?(klass)
|
294
|
+
type.new(self.class.const_get(klass))
|
295
|
+
else
|
296
|
+
raise DoubleFailure, "#{klass} is not a valid class name"
|
297
|
+
end
|
34
298
|
|
35
|
-
|
299
|
+
super
|
300
|
+
end
|
301
|
+
end
|
36
302
|
end
|
37
303
|
|
38
|
-
|
304
|
+
|
305
|
+
# ### RSpec Integration
|
306
|
+
#
|
307
|
+
# This RSpec adapter shows two useful techniques: dynamic library loading
|
308
|
+
# which removes RSpec as a direct dependency, and use of the `mixin`
|
309
|
+
# method to further extend the target evalutor.
|
310
|
+
module RSpecExpectations
|
311
|
+
def self.included(mod)
|
312
|
+
begin
|
313
|
+
require 'rspec/expectations'
|
314
|
+
require 'rspec/matchers'
|
315
|
+
rescue LoadError
|
316
|
+
raise "RSpec is not available, cannot use RSpec assertion context."
|
317
|
+
end
|
318
|
+
|
319
|
+
mod.include(RSpec::Matchers)
|
320
|
+
end
|
321
|
+
|
322
|
+
def call(unit_of_work)
|
323
|
+
super
|
324
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
325
|
+
[Failure.new(unit_of_work, e.message, e.backtrace)]
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
DEFAULT = stack do
|
330
|
+
include Simple
|
331
|
+
include Doubles
|
332
|
+
end
|
39
333
|
end
|
40
334
|
end
|