muack 0.5.0
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.
- 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
|