quacky 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.
- data/VERSION +1 -0
- data/lib/quacky.rb +2 -0
- data/lib/quacky/quacky.rb +191 -0
- data/lib/quacky/rspec_setup.rb +19 -0
- data/spec/lib/quacky_spec.rb +238 -0
- metadata +61 -0
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/lib/quacky.rb
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
require 'active_support/core_ext/module/aliasing'
|
2
|
+
|
3
|
+
module Quacky
|
4
|
+
class DuckTypeVerificationFailure < RuntimeError; end
|
5
|
+
|
6
|
+
class DuckTypeVerifier
|
7
|
+
def initialize duck_type
|
8
|
+
@duck_type = duck_type
|
9
|
+
end
|
10
|
+
|
11
|
+
def verify!(object)
|
12
|
+
duck_type_methods.each do |method|
|
13
|
+
raise Quacky::DuckTypeVerificationFailure, "object does not respond to `#{method.name}'" unless object.respond_to?(method.name)
|
14
|
+
|
15
|
+
target_method = object.public_method(method.name)
|
16
|
+
if target_method.parameters.count != method.parameters.count ||
|
17
|
+
target_method.parameters.map {|p| p.first } != method.parameters.map {|p| p.first}
|
18
|
+
raise Quacky::DuckTypeVerificationFailure, "method signatures differ"
|
19
|
+
end
|
20
|
+
|
21
|
+
true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
attr_reader :duck_type
|
27
|
+
|
28
|
+
def duck_type_methods
|
29
|
+
@duck_type_methods ||= (duck_type_object.methods - Object.methods).map do |method_name|
|
30
|
+
@duck_type_object.public_method(method_name)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def duck_type_object
|
35
|
+
return @duck_type_object if @duck_type_object
|
36
|
+
duck_type_class = Class.new
|
37
|
+
duck_type_class.send :include, duck_type
|
38
|
+
@duck_type_object = duck_type_class.new
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module Quacky
|
44
|
+
extend self
|
45
|
+
class Double; end
|
46
|
+
|
47
|
+
class NoMethodError < RuntimeError; end
|
48
|
+
class MethodSignatureMismatch < ArgumentError; end
|
49
|
+
class UnexpectedArguments < ArgumentError; end
|
50
|
+
class UnsatisfiedExpectation < ArgumentError; end
|
51
|
+
|
52
|
+
def expectations
|
53
|
+
@expectations ||= []
|
54
|
+
end
|
55
|
+
|
56
|
+
def clear_expectations!
|
57
|
+
@expectations = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
class Stub
|
61
|
+
def initialize method
|
62
|
+
@method = method
|
63
|
+
end
|
64
|
+
|
65
|
+
def with *args
|
66
|
+
@expected_args = args
|
67
|
+
call_through *args
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def and_return value=nil, &block
|
72
|
+
@return_value = value
|
73
|
+
@return_block = block
|
74
|
+
end
|
75
|
+
|
76
|
+
def call *args
|
77
|
+
@called_args = args
|
78
|
+
validate_expectation
|
79
|
+
call_through *args
|
80
|
+
|
81
|
+
if expected_args
|
82
|
+
return_value if (called_args == expected_args)
|
83
|
+
else
|
84
|
+
return_value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def validate_satisfaction!
|
89
|
+
if expected_args
|
90
|
+
if called_args == expected_args
|
91
|
+
true
|
92
|
+
else
|
93
|
+
raise UnsatisfiedExpectation
|
94
|
+
end
|
95
|
+
else
|
96
|
+
was_called? || raise(UnsatisfiedExpectation)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
attr_reader :called_args, :expected_args
|
102
|
+
|
103
|
+
def was_called?
|
104
|
+
!!called_args
|
105
|
+
end
|
106
|
+
|
107
|
+
def validate_expectation
|
108
|
+
if expected_args && called_args != expected_args
|
109
|
+
raise(
|
110
|
+
Quacky::UnexpectedArguments,
|
111
|
+
"#{@method.name} was called with unexpected arguments: #{called_args.join ", "}"
|
112
|
+
)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def return_value
|
117
|
+
return @return_value if @return_value
|
118
|
+
@return_block.call if @return_block
|
119
|
+
end
|
120
|
+
|
121
|
+
def call_through *args
|
122
|
+
begin
|
123
|
+
@method.call *args
|
124
|
+
rescue ArgumentError => e
|
125
|
+
raise Quacky::MethodSignatureMismatch, e.message
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
module Expectations
|
131
|
+
def stub method_name
|
132
|
+
setup_expectation method_name
|
133
|
+
end
|
134
|
+
|
135
|
+
def should_receive method_name
|
136
|
+
stub(method_name).tap do |expectation|
|
137
|
+
Quacky.expectations << expectation
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
def setup_expectation method_name
|
143
|
+
raise Quacky::NoMethodError unless respond_to? method_name
|
144
|
+
instance_variable_set "@#{method_name}_expectation", Stub.new(public_method(method_name))
|
145
|
+
|
146
|
+
eval <<-EVAL
|
147
|
+
class << self
|
148
|
+
define_method("#{method_name}_with_expectation") do |*args|
|
149
|
+
@#{method_name}_expectation.call *args
|
150
|
+
end
|
151
|
+
|
152
|
+
alias_method_chain :#{method_name}, :expectation
|
153
|
+
end
|
154
|
+
EVAL
|
155
|
+
|
156
|
+
instance_variable_get "@#{method_name}_expectation"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def double duck_type
|
161
|
+
Double.new.tap do |object|
|
162
|
+
object.extend duck_type
|
163
|
+
object.extend Quacky::Expectations
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def class_double options
|
168
|
+
class_modules, instance_modules = parse_class_double_options options
|
169
|
+
|
170
|
+
Class.new do
|
171
|
+
class_modules.each do |class_module|
|
172
|
+
extend class_module
|
173
|
+
end
|
174
|
+
|
175
|
+
instance_modules.each do |instance_module|
|
176
|
+
include instance_module
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
def parse_class_double_options options
|
183
|
+
class_modules = options.fetch :class
|
184
|
+
instance_modules = options.fetch :instance
|
185
|
+
|
186
|
+
class_modules = [class_modules] unless class_modules.respond_to? :each
|
187
|
+
instance_modules = [instance_modules] unless instance_modules.respond_to? :each
|
188
|
+
|
189
|
+
[class_modules, instance_modules]
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
if defined? RSpec
|
2
|
+
RSpec::Matchers.define :quack_like do |*expected_duck_types|
|
3
|
+
expected_duck_types.each do |expected_duck_type|
|
4
|
+
match do |actual|
|
5
|
+
Quacky::DuckTypeVerifier.new(expected_duck_type).verify! actual
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.before(:each) do
|
12
|
+
Quacky.clear_expectations!
|
13
|
+
end
|
14
|
+
|
15
|
+
config.after(:each) do
|
16
|
+
Quacky.expectations.map &:validate_satisfaction!
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
require_relative '../../lib/quacky/quacky'
|
2
|
+
|
3
|
+
module Duck
|
4
|
+
def duck arg; end
|
5
|
+
end
|
6
|
+
|
7
|
+
describe Quacky::DuckTypeVerifier do
|
8
|
+
let(:conforming_object) do
|
9
|
+
Class.new do
|
10
|
+
def quack arg1,arg2,arg3=nil; end
|
11
|
+
end.new
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:duck_type_module) do
|
15
|
+
Module.new do
|
16
|
+
def quack a,b,c=nil; end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#verify!" do
|
21
|
+
let(:verifier) { Quacky::DuckTypeVerifier.new(duck_type_module) }
|
22
|
+
|
23
|
+
context "non-conforming objects" do
|
24
|
+
context "an object that doesn't even respond to the same methods" do
|
25
|
+
let(:non_conforming_object) do
|
26
|
+
Class.new.new
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should raise a Quacky::DuckTypeVerificationFailure" do
|
30
|
+
expect { verifier.verify! non_conforming_object }.to raise_exception Quacky::DuckTypeVerificationFailure
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "an object that has the methods but with different parameters" do
|
35
|
+
let(:non_conforming_object) do
|
36
|
+
Class.new do
|
37
|
+
def quack; end
|
38
|
+
end.new
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should raise a Quacky::DuckTypeVerificationFailure" do
|
42
|
+
expect { verifier.verify! non_conforming_object }.to raise_exception Quacky::DuckTypeVerificationFailure
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "given a conforming object" do
|
48
|
+
let(:conforming_object) do
|
49
|
+
Class.new do
|
50
|
+
def quack arg1,arg2,arg3=nil; end
|
51
|
+
end.new
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should return true" do
|
55
|
+
expect { verifier.verify! conforming_object }.not_to raise_exception Quacky::DuckTypeVerificationFailure
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe Quacky do
|
62
|
+
|
63
|
+
describe ".clear_expectations!" do
|
64
|
+
it "should reset .expecatations to an empty collection" do
|
65
|
+
Quacky.expectations.should be_empty
|
66
|
+
Quacky.expectations << "foo"
|
67
|
+
Quacky.clear_expectations!
|
68
|
+
Quacky.expectations.should be_empty
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "#double" do
|
73
|
+
let(:eigenclass) { class << Quacky.double(Duck); self; end }
|
74
|
+
|
75
|
+
subject { eigenclass }
|
76
|
+
|
77
|
+
its(:included_modules) { should include Duck }
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "#class_double" do
|
81
|
+
let(:class_double) { Quacky.class_double class: class_ducks, instance: instance_ducks }
|
82
|
+
let(:class_eigenclass) { class << class_double; self; end }
|
83
|
+
|
84
|
+
shared_examples_for "quacky class double" do
|
85
|
+
context "instance methods" do
|
86
|
+
subject { class_double }
|
87
|
+
its(:ancestors) { should include *instance_ducks }
|
88
|
+
end
|
89
|
+
|
90
|
+
context "class methods" do
|
91
|
+
subject { class_eigenclass }
|
92
|
+
its(:included_modules) { should include *class_ducks }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context "single class and instance module" do
|
97
|
+
let(:class_ducks) { Module.new }
|
98
|
+
let(:instance_ducks) { Module.new }
|
99
|
+
|
100
|
+
it_behaves_like "quacky class double"
|
101
|
+
end
|
102
|
+
|
103
|
+
context "multiple class and instance modules" do
|
104
|
+
let(:class_ducks) { [ Module.new, Module.new ] }
|
105
|
+
let(:instance_ducks) { [ Module.new, Module.new ] }
|
106
|
+
|
107
|
+
it_behaves_like "quacky class double"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe Quacky::Double do
|
112
|
+
let(:q_double) { Quacky.double Duck }
|
113
|
+
let(:expectation) { double(:expectation) }
|
114
|
+
|
115
|
+
describe ".stub" do
|
116
|
+
it "should raise an exception if the method does not already exist on the double" do
|
117
|
+
expect { q_double.stub("random_method_#{rand 1000000}") }.to raise_exception Quacky::NoMethodError
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should initialize and return a new Quacky::Stub otherwise" do
|
121
|
+
Quacky::Stub.should_receive(:new).with(q_double.public_method(:duck)).and_return expectation
|
122
|
+
q_double.stub(:duck).should == expectation
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should reroute calls to the original method to call the expectation's call method" do
|
126
|
+
Quacky::Stub.stub(:new).with(q_double.public_method(:duck)).and_return expectation
|
127
|
+
q_double.stub(:duck)
|
128
|
+
|
129
|
+
argument = double :argument
|
130
|
+
expectation.should_receive(:call).with argument
|
131
|
+
q_double.duck(argument)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe "should_receive" do
|
136
|
+
it "should raise an exception if the method does not already exist on the quacky double" do
|
137
|
+
expect { q_double.should_receive("random_method_#{rand 1000000}") }.to raise_exception Quacky::NoMethodError
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should initialize and return a new QuackyStub otherwise" do
|
141
|
+
Quacky::Stub.should_receive(:new).with(q_double.public_method(:duck)).and_return expectation
|
142
|
+
q_double.should_receive(:duck).should == expectation
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should add the generated expectation to the list of required expectations" do
|
146
|
+
Quacky::Stub.stub(:new).with(q_double.public_method(:duck)).and_return expectation
|
147
|
+
q_double.should_receive(:duck)
|
148
|
+
Quacky.expectations.should include expectation
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
module Quacky
|
155
|
+
describe "mocks" do
|
156
|
+
let(:object) do
|
157
|
+
Class.new do
|
158
|
+
include Duck
|
159
|
+
end.new
|
160
|
+
end
|
161
|
+
|
162
|
+
describe Stub do
|
163
|
+
let(:q_expectation) { Quacky::Stub.new(object.public_method(:duck)) }
|
164
|
+
|
165
|
+
describe "#with" do
|
166
|
+
it "should raise an exception if the original method's signature mismatches" do
|
167
|
+
expect { q_expectation.with 1,2,3 }.to raise_exception Quacky::MethodSignatureMismatch
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
describe "#and_return" do
|
172
|
+
context "static value" do
|
173
|
+
it "should return the static value when called" do
|
174
|
+
return_value = double :return_value
|
175
|
+
q_expectation.and_return return_value
|
176
|
+
q_expectation.call(double :argument).should == return_value
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
context "block return value" do
|
181
|
+
it "should return the value returned by the block" do
|
182
|
+
return_value = double :return_value
|
183
|
+
q_expectation.and_return { return_value }
|
184
|
+
q_expectation.call(double :argument).should == return_value
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
describe "#call" do
|
190
|
+
subject { q_expectation }
|
191
|
+
|
192
|
+
context "with invalid arguments" do
|
193
|
+
it "should raise an exception" do
|
194
|
+
expect{ q_expectation.call(1,2,3)}.to raise_exception Quacky::MethodSignatureMismatch
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context "called with unexpected arguments" do
|
199
|
+
it "should raise a Quacky::UnexpectedArguments exception" do
|
200
|
+
q_expectation.with double(:expected_argument)
|
201
|
+
expect { q_expectation.call double(:unexpected_arguments) }.to raise_exception Quacky::UnexpectedArguments
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
context "called with expected arguments" do
|
206
|
+
it "should return the configured return value" do
|
207
|
+
q_expectation.with("foo").and_return "bar"
|
208
|
+
q_expectation.call("foo").should == "bar"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
describe "validate_satisfaction!" do
|
214
|
+
subject { q_expectation.validate_satisfaction! }
|
215
|
+
|
216
|
+
context "no with expectation" do
|
217
|
+
before { q_expectation.call double(:argument) }
|
218
|
+
specify { expect { subject }.not_to raise_exception }
|
219
|
+
end
|
220
|
+
|
221
|
+
context "not called at all" do
|
222
|
+
specify { expect { subject }.to raise_exception Quacky::UnsatisfiedExpectation }
|
223
|
+
end
|
224
|
+
|
225
|
+
context "with expectation" do
|
226
|
+
let(:argument) { double :argument }
|
227
|
+
|
228
|
+
before { q_expectation.with argument }
|
229
|
+
|
230
|
+
context "called with matching argument" do
|
231
|
+
before { q_expectation.call argument }
|
232
|
+
specify { expect { subject }.not_to raise_exception }
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: quacky
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Matt Parker
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-06-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70164230670820 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70164230670820
|
25
|
+
description:
|
26
|
+
email: moonmaster9000@gmail.com
|
27
|
+
executables: []
|
28
|
+
extensions: []
|
29
|
+
extra_rdoc_files: []
|
30
|
+
files:
|
31
|
+
- lib/quacky/quacky.rb
|
32
|
+
- lib/quacky/rspec_setup.rb
|
33
|
+
- lib/quacky.rb
|
34
|
+
- VERSION
|
35
|
+
- spec/lib/quacky_spec.rb
|
36
|
+
homepage: https://github.com/moonmaster9000/quacky
|
37
|
+
licenses: []
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
requirements: []
|
55
|
+
rubyforge_project:
|
56
|
+
rubygems_version: 1.8.17
|
57
|
+
signing_key:
|
58
|
+
specification_version: 3
|
59
|
+
summary: Ensure your test doubles quack like the real thing.
|
60
|
+
test_files:
|
61
|
+
- spec/lib/quacky_spec.rb
|