xspec 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|