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 ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,2 @@
1
+ require 'quacky/quacky'
2
+ require 'quacky/rspec_setup'
@@ -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