quacky 0.0.1

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