muack 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitmodules +3 -0
- data/.travis.yml +11 -0
- data/CHANGES.md +5 -0
- data/Gemfile +7 -0
- data/LICENSE +201 -0
- data/README.md +494 -0
- data/Rakefile +18 -0
- data/lib/muack.rb +77 -0
- data/lib/muack/any_instance_of.rb +4 -0
- data/lib/muack/definition.rb +5 -0
- data/lib/muack/failure.rb +38 -0
- data/lib/muack/mock.rb +167 -0
- data/lib/muack/modifier.rb +29 -0
- data/lib/muack/proxy.rb +39 -0
- data/lib/muack/satisfy.rb +54 -0
- data/lib/muack/session.rb +12 -0
- data/lib/muack/stub.rb +22 -0
- data/lib/muack/test.rb +26 -0
- data/lib/muack/version.rb +4 -0
- data/muack.gemspec +53 -0
- data/task/.gitignore +1 -0
- data/task/gemgem.rb +268 -0
- data/test/test_any_instance_of.rb +30 -0
- data/test/test_mock.rb +170 -0
- data/test/test_proxy.rb +77 -0
- data/test/test_readme.rb +16 -0
- data/test/test_satisfy.rb +200 -0
- data/test/test_stub.rb +62 -0
- metadata +85 -0
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
require "#{dir = File.dirname(__FILE__)}/task/gemgem"
|
3
|
+
Gemgem.dir = dir
|
4
|
+
|
5
|
+
($LOAD_PATH << File.expand_path("#{Gemgem.dir}/lib")).uniq!
|
6
|
+
|
7
|
+
desc 'Generate gemspec'
|
8
|
+
task 'gem:spec' do
|
9
|
+
Gemgem.spec = Gemgem.create do |s|
|
10
|
+
require 'muack/version'
|
11
|
+
s.name = 'muack'
|
12
|
+
s.version = Muack::VERSION
|
13
|
+
|
14
|
+
%w[].each{ |g| s.add_runtime_dependency(g) }
|
15
|
+
end
|
16
|
+
|
17
|
+
Gemgem.write
|
18
|
+
end
|
data/lib/muack.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
|
2
|
+
require 'muack/mock'
|
3
|
+
require 'muack/stub'
|
4
|
+
require 'muack/proxy'
|
5
|
+
|
6
|
+
require 'muack/satisfy'
|
7
|
+
require 'muack/session'
|
8
|
+
|
9
|
+
require 'muack/any_instance_of'
|
10
|
+
|
11
|
+
module Muack
|
12
|
+
def self.verify
|
13
|
+
session.verify
|
14
|
+
ensure
|
15
|
+
reset
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.session
|
19
|
+
@session ||= Muack::Session.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.reset
|
23
|
+
@session && @session.reset
|
24
|
+
@session = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
module API
|
28
|
+
module_function
|
29
|
+
def mock obj=Object.new
|
30
|
+
ret = Muack.session["mk #{obj.__id__}"] ||= Muack::Mock.new(obj)
|
31
|
+
if block_given? then yield(ret) else ret end
|
32
|
+
end
|
33
|
+
|
34
|
+
def stub obj=Object.new
|
35
|
+
ret = Muack.session["sb #{obj.__id__}"] ||= Muack::Stub.new(obj)
|
36
|
+
if block_given? then yield(ret) else ret end
|
37
|
+
end
|
38
|
+
|
39
|
+
def mock_proxy obj=Object.new
|
40
|
+
ret = Muack.session["mp #{obj.__id__}"] ||= Muack::MockProxy.new(obj)
|
41
|
+
if block_given? then yield(ret) else ret end
|
42
|
+
end
|
43
|
+
|
44
|
+
def stub_proxy obj=Object.new
|
45
|
+
ret = Muack.session["sp #{obj.__id__}"] ||= Muack::StubProxy.new(obj)
|
46
|
+
if block_given? then yield(ret) else ret end
|
47
|
+
end
|
48
|
+
|
49
|
+
def any_instance_of klass
|
50
|
+
yield Muack::AnyInstanceOf.new(klass)
|
51
|
+
end
|
52
|
+
|
53
|
+
def is_a klass
|
54
|
+
Muack::IsA.new(klass)
|
55
|
+
end
|
56
|
+
|
57
|
+
def anything
|
58
|
+
Muack::Anything.new
|
59
|
+
end
|
60
|
+
|
61
|
+
def match regexp
|
62
|
+
Muack::Match.new(regexp)
|
63
|
+
end
|
64
|
+
|
65
|
+
def hash_including hash
|
66
|
+
Muack::HashIncluding.new(hash)
|
67
|
+
end
|
68
|
+
|
69
|
+
def within range_or_array
|
70
|
+
Muack::Within.new(range_or_array)
|
71
|
+
end
|
72
|
+
|
73
|
+
def satisfy &block
|
74
|
+
Muack::Satisfy.new(block)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
|
2
|
+
module Muack
|
3
|
+
class Failure < Exception
|
4
|
+
attr_reader :expected
|
5
|
+
def build_expected obj, expected_defis
|
6
|
+
@expected = expected_defis.uniq{ |d| [d.msg, d.args] }.map{ |defi|
|
7
|
+
"#{obj.inspect}.#{defi.msg}(" \
|
8
|
+
"#{defi.args.map(&:inspect).join(', ')})"
|
9
|
+
}.join("\n or: ")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Unexpected < Failure
|
14
|
+
attr_reader :was
|
15
|
+
def initialize obj, expected_defis, msg, args
|
16
|
+
@was = "#{obj.inspect}.#{msg}(" \
|
17
|
+
"#{args.map(&:inspect).join(', ')})"
|
18
|
+
if expected_defis.empty?
|
19
|
+
super("\nExpected: #{@was}\n was not called.")
|
20
|
+
else
|
21
|
+
build_expected(obj, expected_defis)
|
22
|
+
super("\nExpected: #{expected}\n but was: #{was}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Expected < Failure
|
28
|
+
attr_reader :expected_times, :actual_times
|
29
|
+
def initialize obj, defi, expected_times, actual_times
|
30
|
+
build_expected(obj, [defi])
|
31
|
+
@expected_times = expected_times
|
32
|
+
@actual_times = actual_times
|
33
|
+
|
34
|
+
super("\nExpected: #{expected}\n called #{expected_times} times\n" \
|
35
|
+
" but was #{actual_times} times.")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/muack/mock.rb
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
|
2
|
+
require 'muack/definition'
|
3
|
+
require 'muack/modifier'
|
4
|
+
require 'muack/failure'
|
5
|
+
|
6
|
+
module Muack
|
7
|
+
class Mock < BasicObject
|
8
|
+
attr_reader :object
|
9
|
+
def initialize object
|
10
|
+
@object = object
|
11
|
+
@__mock_ignore = []
|
12
|
+
[:__mock_defis=, :__mock_disps=].each do |m|
|
13
|
+
__send__(m, ::Hash.new{ |h, k| h[k] = [] })
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Public API: Bacon needs this, or we often ended up with stack overflow
|
18
|
+
def inspect
|
19
|
+
"#<#{class << self; self; end.superclass} object=#{object.inspect}>"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Public API: Define mocked method
|
23
|
+
def method_missing msg, *args, &block
|
24
|
+
defi = Definition.new(msg, args, block)
|
25
|
+
__mock_inject_method(defi) if __mock_pure?(defi)
|
26
|
+
__mock_defis_push(defi)
|
27
|
+
Modifier.new(self, defi)
|
28
|
+
end
|
29
|
+
|
30
|
+
# used for Muack::Modifier#times
|
31
|
+
def __mock_defis_push defi
|
32
|
+
__mock_defis[defi.msg] << defi
|
33
|
+
end
|
34
|
+
|
35
|
+
# used for Muack::Modifier#times
|
36
|
+
def __mock_defis_pop defi
|
37
|
+
__mock_defis[defi.msg].pop
|
38
|
+
end
|
39
|
+
|
40
|
+
# used for Muack::Modifier#times
|
41
|
+
def __mock_ignore defi
|
42
|
+
@__mock_ignore << defi
|
43
|
+
end
|
44
|
+
|
45
|
+
# used for mocked object to dispatch mocked method
|
46
|
+
def __mock_dispatch msg, actual_args, actual_block
|
47
|
+
if defi = __mock_defis[msg].shift
|
48
|
+
__mock_disps_push(defi)
|
49
|
+
if __mock_check_args(defi.args, actual_args)
|
50
|
+
__mock_block_call(defi, actual_args, actual_block)
|
51
|
+
else
|
52
|
+
Mock.__send__(:raise, # Wrong argument
|
53
|
+
Unexpected.new(object, [defi], msg, actual_args))
|
54
|
+
end
|
55
|
+
else
|
56
|
+
defis = __mock_disps[msg]
|
57
|
+
if expected_defi = defis.find{ |d| d.args == actual_args }
|
58
|
+
Mock.__send__(:raise, # Too many times
|
59
|
+
Expected.new(object, expected_defi, defis.size, defis.size+1))
|
60
|
+
else
|
61
|
+
Mock.__send__(:raise, # Wrong argument
|
62
|
+
Unexpected.new(object, defis, msg, actual_args))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# used for Muack::Session#verify
|
68
|
+
def __mock_verify
|
69
|
+
__mock_defis.values.all?(&:empty?) || begin
|
70
|
+
msg, defis_with_same_msg = __mock_defis.find{ |_, v| v.size > 0 }
|
71
|
+
args, defis = defis_with_same_msg.group_by(&:args).first
|
72
|
+
dsize = __mock_disps[msg].select{ |d| d.args == args }.size
|
73
|
+
Mock.__send__(:raise, # Too little times
|
74
|
+
Expected.new(object, defis.first, defis.size + dsize, dsize))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# used for Muack::Session#reset
|
79
|
+
def __mock_reset
|
80
|
+
[__mock_defis.values, __mock_disps.values, @__mock_ignore].
|
81
|
+
flatten.compact.each do |defi|
|
82
|
+
object.singleton_class.module_eval do
|
83
|
+
methods = instance_methods(false)
|
84
|
+
if methods.include?(defi.msg) # removed mocked method
|
85
|
+
remove_method(defi.msg) # could be removed by other defi
|
86
|
+
end
|
87
|
+
if methods.include?(defi.original_method) # restore original method
|
88
|
+
alias_method defi.msg, defi.original_method
|
89
|
+
remove_method defi.original_method
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
protected # get warnings for private attributes
|
96
|
+
attr_accessor :__mock_defis, :__mock_disps
|
97
|
+
|
98
|
+
private
|
99
|
+
def __mock_pure? defi
|
100
|
+
__mock_defis[defi.msg].empty? && __mock_disps[defi.msg].empty?
|
101
|
+
end
|
102
|
+
|
103
|
+
def __mock_inject_method defi
|
104
|
+
target = object.singleton_class
|
105
|
+
Mock.store_original_method(target, defi)
|
106
|
+
__mock_inject_mock_method(target, defi)
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.store_original_method klass, defi
|
110
|
+
return unless klass.instance_methods(false).include?(defi.msg)
|
111
|
+
# store original method
|
112
|
+
original_method = find_new_name(klass, defi.msg)
|
113
|
+
klass.__send__(:alias_method, original_method, defi.msg)
|
114
|
+
defi.original_method = original_method
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.find_new_name klass, message, level=0
|
118
|
+
raise "Cannot find a suitable method name, tried #{level+1} times." if
|
119
|
+
level >= 9
|
120
|
+
|
121
|
+
new_name = "__muack_mock_#{level}_#{message}".to_sym
|
122
|
+
if klass.instance_methods(false).include?(new_name)
|
123
|
+
find_new_name(klass, message, level+1)
|
124
|
+
else
|
125
|
+
new_name
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def __mock_inject_mock_method target, defi
|
130
|
+
mock = self # remember the context
|
131
|
+
target.__send__(:define_method, defi.msg){|*actual_args, &actual_block|
|
132
|
+
mock.__mock_dispatch(defi.msg, actual_args, actual_block)
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
def __mock_block_call defi, actual_args, actual_block
|
137
|
+
if block = defi.block
|
138
|
+
arity = block.arity
|
139
|
+
if arity < 0
|
140
|
+
block.call(*actual_args , &actual_block)
|
141
|
+
else
|
142
|
+
block.call(*actual_args.first(arity), &actual_block)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def __mock_check_args expected_args, actual_args
|
148
|
+
if expected_args == [WithAnyArgs]
|
149
|
+
true
|
150
|
+
elsif expected_args.none?{ |arg| arg.kind_of?(Satisfy) }
|
151
|
+
expected_args == actual_args
|
152
|
+
|
153
|
+
elsif expected_args.size == actual_args.size
|
154
|
+
expected_args.zip(actual_args).all?{ |(e, a)|
|
155
|
+
if e.kind_of?(Satisfy) then e.match(a) else e == a end
|
156
|
+
}
|
157
|
+
else
|
158
|
+
false
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# used for Muack::Mock#__mock_dispatch
|
163
|
+
def __mock_disps_push defi
|
164
|
+
__mock_disps[defi.msg] << defi
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
require 'muack/definition'
|
3
|
+
|
4
|
+
module Muack
|
5
|
+
class Modifier < Struct.new(:mock, :defi)
|
6
|
+
# Public API
|
7
|
+
def with_any_args
|
8
|
+
defi.args = [WithAnyArgs]
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
# Public API
|
13
|
+
def times number
|
14
|
+
if number >= 1
|
15
|
+
(number - 1).times{ mock.__mock_defis_push(defi) }
|
16
|
+
elsif number == 0
|
17
|
+
mock.__mock_ignore(mock.__mock_defis_pop(defi))
|
18
|
+
else
|
19
|
+
raise "What would you expect from calling a method #{number} times?"
|
20
|
+
end
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
# Public API
|
25
|
+
def object
|
26
|
+
mock.object
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/muack/proxy.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
require 'muack/mock'
|
3
|
+
require 'muack/stub'
|
4
|
+
|
5
|
+
module Muack
|
6
|
+
module Proxy
|
7
|
+
def __mock_block_call defi, actual_args, actual_block
|
8
|
+
# handle block call in injected method, since we need to call origin
|
9
|
+
defi # but we still want to know which defi gets dispatched!
|
10
|
+
end
|
11
|
+
|
12
|
+
def __mock_inject_mock_method target, defi
|
13
|
+
mock = self # remember the context
|
14
|
+
target.__send__(:define_method, defi.msg){|*actual_args, &actual_block|
|
15
|
+
d = mock.__mock_dispatch(defi.msg, actual_args, actual_block)
|
16
|
+
|
17
|
+
ret = if d.original_method
|
18
|
+
__send__(d.original_method, *actual_args, &actual_block)
|
19
|
+
else
|
20
|
+
super(*actual_args, &actual_block)
|
21
|
+
end
|
22
|
+
|
23
|
+
if d.block
|
24
|
+
d.block.call(ret)
|
25
|
+
else
|
26
|
+
ret
|
27
|
+
end
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class MockProxy < Mock
|
33
|
+
include Proxy
|
34
|
+
end
|
35
|
+
|
36
|
+
class StubProxy < Stub
|
37
|
+
include Proxy
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
|
2
|
+
module Muack
|
3
|
+
class Satisfy < Struct.new(:block, :api_args)
|
4
|
+
def match actual_arg
|
5
|
+
!!block.call(actual_arg)
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_s
|
9
|
+
"Muack::API.#{api_name}(#{api_args.map(&:inspect).join(', ')})"
|
10
|
+
end
|
11
|
+
alias_method :inspect, :to_s
|
12
|
+
|
13
|
+
def api_name
|
14
|
+
self.class.name[/::(\w+)$/, 1].
|
15
|
+
gsub(/([A-Z][a-z]*)+?(?=[A-Z][a-z]*)/, '\\1_').downcase
|
16
|
+
end
|
17
|
+
|
18
|
+
def api_args
|
19
|
+
super || [block]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class IsA < Satisfy
|
24
|
+
def initialize klass
|
25
|
+
super lambda{ |actual_arg| actual_arg.kind_of?(klass) }, [klass]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Anything < Satisfy
|
30
|
+
def initialize
|
31
|
+
super lambda{ |_| true }, []
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Match < Satisfy
|
36
|
+
def initialize regexp
|
37
|
+
super lambda{ |actual_arg| regexp.match(actual_arg) }, [regexp]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class HashIncluding < Satisfy
|
42
|
+
def initialize hash
|
43
|
+
super lambda{ |actual_arg|
|
44
|
+
actual_arg.values_at(*hash.keys) == hash.values }, [hash]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Within < Satisfy
|
49
|
+
def initialize range_or_array
|
50
|
+
super lambda{ |actual_arg| range_or_array.include?(actual_arg) },
|
51
|
+
[range_or_array]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|