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