caricature 0.3.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Rakefile +100 -0
- data/VERSION +1 -0
- data/caricature.gemspec +177 -0
- data/doc/Array.html +251 -0
- data/doc/Caricature/ArgumentRecording.html +366 -0
- data/doc/Caricature/ClrClassDescriptor.html +303 -0
- data/doc/Caricature/ClrClassMessenger.html +263 -0
- data/doc/Caricature/ClrInterfaceDescriptor.html +295 -0
- data/doc/Caricature/ClrInterfaceIsolator.html +365 -0
- data/doc/Caricature/ClrInterfaceMessenger.html +260 -0
- data/doc/Caricature/ClrIsolator.html +424 -0
- data/doc/Caricature/Expectation.html +549 -0
- data/doc/Caricature/ExpectationBuilder.html +308 -0
- data/doc/Caricature/ExpectationSyntax.html +438 -0
- data/doc/Caricature/Expectations.html +344 -0
- data/doc/Caricature/Interception/ClassMethods.html +246 -0
- data/doc/Caricature/Interception.html +416 -0
- data/doc/Caricature/Isolation.html +578 -0
- data/doc/Caricature/Isolator.html +519 -0
- data/doc/Caricature/MemberDescriptor.html +335 -0
- data/doc/Caricature/Messenger.html +335 -0
- data/doc/Caricature/MethodCallRecorder.html +440 -0
- data/doc/Caricature/MethodCallRecording.html +493 -0
- data/doc/Caricature/RubyIsolator.html +416 -0
- data/doc/Caricature/RubyMessenger.html +260 -0
- data/doc/Caricature/RubyObjectDescriptor.html +289 -0
- data/doc/Caricature/TypeDescriptor.html +378 -0
- data/doc/Caricature/Verification.html +443 -0
- data/doc/Caricature.html +358 -0
- data/doc/Class.html +287 -0
- data/doc/Hash.html +255 -0
- data/doc/Module.html +287 -0
- data/doc/Object.html +322 -0
- data/doc/README_markdown.html +241 -0
- data/doc/String.html +289 -0
- data/doc/System/String.html +289 -0
- data/doc/System/Type.html +289 -0
- data/doc/System.html +207 -0
- data/doc/created.rid +1 -0
- data/doc/images/brick.png +0 -0
- data/doc/images/brick_link.png +0 -0
- data/doc/images/bug.png +0 -0
- data/doc/images/bullet_black.png +0 -0
- data/doc/images/bullet_toggle_minus.png +0 -0
- data/doc/images/bullet_toggle_plus.png +0 -0
- data/doc/images/date.png +0 -0
- data/doc/images/find.png +0 -0
- data/doc/images/loadingAnimation.gif +0 -0
- data/doc/images/macFFBgHack.png +0 -0
- data/doc/images/package.png +0 -0
- data/doc/images/page_green.png +0 -0
- data/doc/images/page_white_text.png +0 -0
- data/doc/images/page_white_width.png +0 -0
- data/doc/images/plugin.png +0 -0
- data/doc/images/ruby.png +0 -0
- data/doc/images/tag_green.png +0 -0
- data/doc/images/wrench.png +0 -0
- data/doc/images/wrench_orange.png +0 -0
- data/doc/images/zoom.png +0 -0
- data/doc/index.html +312 -0
- data/doc/js/darkfish.js +116 -0
- data/doc/js/jquery.js +32 -0
- data/doc/js/quicksearch.js +114 -0
- data/doc/js/thickbox-compressed.js +10 -0
- data/doc/lib/caricature/clr/descriptor_rb.html +52 -0
- data/doc/lib/caricature/clr/isolation_rb.html +52 -0
- data/doc/lib/caricature/clr/isolator_rb.html +52 -0
- data/doc/lib/caricature/clr/messenger_rb.html +52 -0
- data/doc/lib/caricature/clr_rb.html +52 -0
- data/doc/lib/caricature/descriptor_rb.html +52 -0
- data/doc/lib/caricature/expectation_rb.html +52 -0
- data/doc/lib/caricature/isolation_rb.html +52 -0
- data/doc/lib/caricature/isolator_rb.html +52 -0
- data/doc/lib/caricature/messaging_rb.html +52 -0
- data/doc/lib/caricature/method_call_recorder_rb.html +52 -0
- data/doc/lib/caricature/verification_rb.html +52 -0
- data/doc/lib/caricature_rb.html +52 -0
- data/doc/lib/core_ext/array_rb.html +52 -0
- data/doc/lib/core_ext/class_rb.html +52 -0
- data/doc/lib/core_ext/core_ext_rb.html +52 -0
- data/doc/lib/core_ext/hash_rb.html +52 -0
- data/doc/lib/core_ext/module_rb.html +52 -0
- data/doc/lib/core_ext/object_rb.html +52 -0
- data/doc/lib/core_ext/string_rb.html +52 -0
- data/doc/lib/core_ext/system/string_rb.html +52 -0
- data/doc/lib/core_ext/system/type_rb.html +52 -0
- data/doc/rdoc.css +696 -0
- data/irb_init.rb +9 -0
- data/lib/bin/Workarounds.dll +0 -0
- data/lib/bin/Workarounds.pdb +0 -0
- data/lib/caricature/clr/descriptor.rb +55 -0
- data/lib/caricature/clr/isolation.rb +33 -0
- data/lib/caricature/clr/isolator.rb +112 -0
- data/lib/caricature/clr/messenger.rb +46 -0
- data/lib/caricature/clr.rb +6 -0
- data/lib/caricature/descriptor.rb +74 -0
- data/lib/caricature/expectation.rb +159 -0
- data/lib/caricature/isolation.rb +146 -0
- data/lib/caricature/isolator.rb +287 -0
- data/lib/caricature/messenger.rb +57 -0
- data/lib/caricature/method_call_recorder.rb +130 -0
- data/lib/caricature/verification.rb +43 -0
- data/lib/caricature.rb +11 -0
- data/lib/core_ext/array.rb +10 -0
- data/lib/core_ext/class.rb +15 -0
- data/lib/core_ext/core_ext.rb +8 -0
- data/lib/core_ext/hash.rb +13 -0
- data/lib/core_ext/module.rb +15 -0
- data/lib/core_ext/object.rb +19 -0
- data/lib/core_ext/string.rb +17 -0
- data/lib/core_ext/system/string.rb +21 -0
- data/lib/core_ext/system/type.rb +21 -0
- data/pkg/.gitignore +0 -0
- data/pkg/caricature-0.1.0.gem +0 -0
- data/spec/bacon_helper.rb +49 -3
- data/spec/bin/.gitignore +0 -0
- data/spec/core_ext_spec.rb +9 -0
- data/spec/descriptor_spec.rb +142 -0
- data/spec/expectation_spec.rb +11 -7
- data/spec/integration_spec.rb +94 -2
- data/spec/isolation_spec.rb +0 -42
- data/spec/isolator_spec.rb +72 -66
- data/spec/messaging_spec.rb +172 -0
- data/spec/method_call_spec.rb +21 -21
- data/spec/models/ClrModels.cs +185 -0
- data/spec/verification_spec.rb +1 -1
- data/workarounds/ReflectionHelper.cs +30 -0
- metadata +133 -2
@@ -0,0 +1,55 @@
|
|
1
|
+
module Caricature
|
2
|
+
|
3
|
+
# describes clr interfaces.
|
4
|
+
# Because CLR interfaces can't have static members this descriptor does not collect any class members
|
5
|
+
class ClrInterfaceDescriptor < TypeDescriptor
|
6
|
+
|
7
|
+
# collects instance members on this interface
|
8
|
+
# it will collect properties, methods and property setters
|
9
|
+
def initialize_instance_members_for(klass)
|
10
|
+
clr_type = klass.to_clr_type
|
11
|
+
|
12
|
+
properties = clr_type.collect_interface_properties
|
13
|
+
methods = clr_type.collect_interface_methods
|
14
|
+
|
15
|
+
@instance_members += methods.collect { |mi| MemberDescriptor.new(mi.name.underscore, mi.return_type) }
|
16
|
+
@instance_members += properties.collect { |pi| MemberDescriptor.new(pi.name.underscore, pi.property_type) }
|
17
|
+
@instance_members += properties.select { |pi| pi.can_write }.collect { |pi| MemberDescriptor.new("#{pi.name.underscore}=", nil) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# this method is empty because an interface can't have static members
|
21
|
+
def initialize_class_members_for(klass); end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# Describes a CLR class type. it collects the properties and methods on an instance as well as on a static level
|
27
|
+
class ClrClassDescriptor < TypeDescriptor
|
28
|
+
|
29
|
+
# collects all the instance members of the provided CLR class type
|
30
|
+
def initialize_instance_members_for(klass)
|
31
|
+
clr_type = klass.to_clr_type
|
32
|
+
|
33
|
+
methods = Workarounds::ReflectionHelper.get_instance_methods(clr_type)
|
34
|
+
properties = Workarounds::ReflectionHelper.get_instance_properties(clr_type)
|
35
|
+
|
36
|
+
@instance_members += methods.collect { |mi| MemberDescriptor.new(mi.name.underscore, mi.return_type) }
|
37
|
+
@instance_members += properties.collect { |pi| MemberDescriptor.new(pi.name.underscore, pi.property_type) }
|
38
|
+
@instance_members += properties.select{|pi| pi.can_write }.collect { |pi| MemberDescriptor.new("#{pi.name.underscore}=", nil) }
|
39
|
+
end
|
40
|
+
|
41
|
+
# collects all the static members of the provided CLR class type
|
42
|
+
def initialize_class_members_for(klass)
|
43
|
+
clr_type = klass.to_clr_type
|
44
|
+
|
45
|
+
methods = Workarounds::ReflectionHelper.get_class_methods(clr_type)
|
46
|
+
properties = Workarounds::ReflectionHelper.get_class_properties(clr_type)
|
47
|
+
|
48
|
+
@class_members += methods.collect { |mi| MemberDescriptor.new(mi.name.underscore, mi.return_type, false) }
|
49
|
+
@class_members += properties.collect { |pi| MemberDescriptor.new(pi.name.underscore, pi.property_type, false) }
|
50
|
+
@class_members += properties.select{|pi| pi.can_write }.collect { |pi| MemberDescriptor.new("#{pi.name.underscore}=", nil, false) }
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Caricature
|
2
|
+
|
3
|
+
class Isolation
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
# Creates an isolation object complete with proxy and method call recorder
|
8
|
+
# It works out which isolation it needs to create and provide and initializes the
|
9
|
+
# method call recorder
|
10
|
+
def for(subject, recorder = MethodCallRecorder.new, expectations = Expectations.new)
|
11
|
+
context = IsolatorContext.new subject, recorder, expectations
|
12
|
+
isolation_strategy = subject.is_clr_type? ? get_clr_isolation_strategy(subject) : RubyIsolator
|
13
|
+
|
14
|
+
isolator = isolation_strategy.for context
|
15
|
+
isolation = new(isolator, context)
|
16
|
+
isolator.isolation
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# decides which startegy to use for mocking a CLR object.
|
22
|
+
# When the provided subject is an interface it will return a +ClrInterfaceIsolator+
|
23
|
+
# otherwise it will return a +ClrIsolator+
|
24
|
+
def get_clr_isolation_strategy(subject)
|
25
|
+
return ClrInterfaceIsolator if subject.respond_to? :class_eval and !subject.respond_to? :new
|
26
|
+
ClrIsolator
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Caricature
|
2
|
+
|
3
|
+
# A proxy to CLR objects that records method calls.
|
4
|
+
# this implements all the public instance methods of the class when you use it in ruby code
|
5
|
+
# When you use it in a CLR class you're bound to CLR rules and it only overrides the methods
|
6
|
+
# that are marked as virtual. We also can't isolate static or sealed types at the moment.
|
7
|
+
class ClrIsolator < Isolator
|
8
|
+
|
9
|
+
|
10
|
+
# Implementation of the template method that creates
|
11
|
+
# an isolator for a class defined in a CLR language.
|
12
|
+
def initialize(context)
|
13
|
+
super
|
14
|
+
instance = nil
|
15
|
+
sklass = context.subject
|
16
|
+
unless context.subject.respond_to?(:class_eval)
|
17
|
+
sklass = context.subject.class
|
18
|
+
instance = context.subject
|
19
|
+
end
|
20
|
+
@descriptor = ClrClassDescriptor.new sklass
|
21
|
+
build_isolation sklass, (instance || sklass.new)
|
22
|
+
end
|
23
|
+
|
24
|
+
# initializes the messaging strategy for the isolator
|
25
|
+
def initialize_messenger
|
26
|
+
@context.messenger = ClrClassMessenger.new @context.expectations, @subject
|
27
|
+
end
|
28
|
+
|
29
|
+
# builds the Isolator class for the specified subject
|
30
|
+
def create_isolation_for(subj)
|
31
|
+
members = @descriptor.instance_members
|
32
|
+
class_members = @descriptor.class_members
|
33
|
+
|
34
|
+
klass = Object.const_set(class_name(subj), Class.new(subj))
|
35
|
+
klass.class_eval do
|
36
|
+
|
37
|
+
include Interception
|
38
|
+
|
39
|
+
# access to the proxied subject
|
40
|
+
def ___super___
|
41
|
+
isolation_context.instance
|
42
|
+
end
|
43
|
+
|
44
|
+
members.each do |mem|
|
45
|
+
nm = mem.name.to_s.to_sym
|
46
|
+
define_method nm do |*args|
|
47
|
+
b = nil
|
48
|
+
b = Proc.new { yield } if block_given?
|
49
|
+
isolation_context.send_message(nm, mem.return_type, *args, &b)
|
50
|
+
end unless nm == :to_string
|
51
|
+
end
|
52
|
+
|
53
|
+
class_members.each do |mn|
|
54
|
+
mn = mn.name.to_s.to_sym
|
55
|
+
define_cmethod mn do |*args|
|
56
|
+
b = nil
|
57
|
+
b = Proc.new { yield } if block_given?
|
58
|
+
isolation_context.send_class_message(mn, nil, *args, &b)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
klass
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
# An +Isolator+ for CLR interfaces.
|
70
|
+
# this implements all the methods that are defined on the interface.
|
71
|
+
class ClrInterfaceIsolator < Isolator
|
72
|
+
|
73
|
+
# Implementation of the template method that creates
|
74
|
+
# an isolator for an interface defined in a CLR language.
|
75
|
+
def initialize(context)
|
76
|
+
super
|
77
|
+
sklass = context.subject
|
78
|
+
@descriptor = ClrInterfaceDescriptor.new sklass
|
79
|
+
build_isolation sklass
|
80
|
+
end
|
81
|
+
|
82
|
+
# initializes the messaging strategy for the isolator
|
83
|
+
def initialize_messenger
|
84
|
+
@context.messenger = ClrInterfaceMessenger.new @context.expectations
|
85
|
+
end
|
86
|
+
|
87
|
+
# builds the actual +isolator+ for the CLR interface
|
88
|
+
def create_isolation_for(subj)
|
89
|
+
proxy_members = @descriptor.instance_members
|
90
|
+
|
91
|
+
klass = Object.const_set(class_name(subj), Class.new)
|
92
|
+
klass.class_eval do
|
93
|
+
|
94
|
+
include subj
|
95
|
+
include Interception
|
96
|
+
|
97
|
+
proxy_members.each do |mem|
|
98
|
+
nm = mem.name.to_s.to_sym
|
99
|
+
define_method nm do |*args|
|
100
|
+
b = nil
|
101
|
+
b = Proc.new { yield } if block_given?
|
102
|
+
isolation_context.send_message(nm, mem.return_type, *args, &b)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
klass
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Caricature
|
2
|
+
|
3
|
+
# Encapsulates sending messages to CLR class or instance isolations
|
4
|
+
class ClrClassMessenger < Messenger
|
5
|
+
|
6
|
+
protected
|
7
|
+
|
8
|
+
# deliver the message to the receiving isolation
|
9
|
+
def internal_deliver(mode, method_name, return_type, *args, &b)
|
10
|
+
exp = expectations.find(method_name, mode, *args)
|
11
|
+
if exp
|
12
|
+
res = instance.__send__(method_name, *args, &b) if exp.super_before?
|
13
|
+
res = exp.execute *args
|
14
|
+
res = instance.__send__(method_name, *args, &b) if !exp.super_before? and exp.call_super?
|
15
|
+
res
|
16
|
+
else
|
17
|
+
rt = nil
|
18
|
+
is_value_type = return_type && return_type != System::Void.to_clr_type && return_type.is_value_type
|
19
|
+
rt = System::Activator.create_instance(return_type) if is_value_type
|
20
|
+
rt
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
# Encapsulates sending messages to CLR interface isolations
|
27
|
+
class ClrInterfaceMessenger < Messenger
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
# deliver the message to the receiving isolation
|
32
|
+
def internal_deliver(mode, method_name, return_type, *args, &b)
|
33
|
+
exp = expectations.find(method_name, mode, *args)
|
34
|
+
if exp
|
35
|
+
res = exp.execute *args
|
36
|
+
res
|
37
|
+
else
|
38
|
+
rt = nil
|
39
|
+
rt = System::Activator.create_instance(return_type) if return_type && return_type != System::Void.to_clr_type && return_type.is_value_type
|
40
|
+
rt
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
load_assembly 'Workarounds, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + "/clr/descriptor"
|
4
|
+
require File.dirname(__FILE__) + "/clr/messenger"
|
5
|
+
require File.dirname(__FILE__) + "/clr/isolator"
|
6
|
+
require File.dirname(__FILE__) + "/clr/isolation"
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Caricature
|
2
|
+
|
3
|
+
# Describes a class/type so it can be proxied more easily
|
4
|
+
# This is the base class from which other more specialised descriptors can be
|
5
|
+
# used to provide the metadata need to generate the proxy classes
|
6
|
+
class TypeDescriptor
|
7
|
+
|
8
|
+
# Gets the instance members of the described type/class
|
9
|
+
attr_reader :instance_members
|
10
|
+
|
11
|
+
# Gets the class members for the described type/class
|
12
|
+
attr_reader :class_members
|
13
|
+
|
14
|
+
# initializes a new instance of a type descriptor
|
15
|
+
def initialize(klass)
|
16
|
+
@instance_members = []
|
17
|
+
@class_members = []
|
18
|
+
|
19
|
+
unless klass == :in_unit_test_for_class
|
20
|
+
initialize_instance_members_for klass
|
21
|
+
initialize_class_members_for klass
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# collects the instance members of the provided type
|
26
|
+
def initialize_instance_members_for(klass)
|
27
|
+
raise NotImplementedError.new("Override in implementing class")
|
28
|
+
end
|
29
|
+
|
30
|
+
# collects the class members of the provided type
|
31
|
+
def initialize_class_members_for(klass)
|
32
|
+
raise NotImplementedError.new("Override in implementing class")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Describes a member of a class
|
37
|
+
# this contains the metadata needed to generate the member
|
38
|
+
# and perhaps a default value for the property type if necessary
|
39
|
+
class MemberDescriptor
|
40
|
+
|
41
|
+
# the return type of this member
|
42
|
+
attr_reader :return_type
|
43
|
+
|
44
|
+
# the name of this member
|
45
|
+
attr_reader :name
|
46
|
+
|
47
|
+
# initializes a nem instance of member descriptor
|
48
|
+
def initialize(name, return_type=nil, is_instance_member=true)
|
49
|
+
@name, @return_type, @instance_member = name, return_type, is_instance_member
|
50
|
+
end
|
51
|
+
|
52
|
+
# indicates whether this member is a class member or an instance member
|
53
|
+
def instance_member?
|
54
|
+
@instance_member
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
# Describes a ruby object.
|
60
|
+
class RubyObjectDescriptor < TypeDescriptor
|
61
|
+
|
62
|
+
# collects all the members that are defined by this class
|
63
|
+
def initialize_instance_members_for(klass)
|
64
|
+
@instance_members += klass.instance_methods(false).collect { |mn| MemberDescriptor.new(mn) }
|
65
|
+
end
|
66
|
+
|
67
|
+
# collects all the members that aren't a member of Object.singleton_methods
|
68
|
+
def initialize_class_members_for(klass)
|
69
|
+
@class_members += klass.methods(false).collect { |mn| MemberDescriptor.new(mn) }
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module Caricature
|
2
|
+
|
3
|
+
# A collection of expectations with some methods to make it easier to work with them.
|
4
|
+
# It allows you to add and find expectations based on certain criteria.
|
5
|
+
class Expectations
|
6
|
+
|
7
|
+
#initializes a new empty instance of the +Expectation+ collection
|
8
|
+
def initialize
|
9
|
+
@instance_expectations = []
|
10
|
+
@class_expectations = []
|
11
|
+
end
|
12
|
+
|
13
|
+
# Adds an expectation to this collection. From then on it can be found in the collection.
|
14
|
+
def add_expectation(expectation, mode=:instance)
|
15
|
+
@instance_expectations << expectation unless mode == :class
|
16
|
+
@class_expectations << expectation if mode == :class
|
17
|
+
end
|
18
|
+
|
19
|
+
# Finds an expectation in the collection. It matches by +method_name+ first.
|
20
|
+
# Then it tries to figure out if you care about the arguments. You can say you don't care by providing
|
21
|
+
# the symbol +:any+ as first argument to this method. When you don't care the first match is being returned
|
22
|
+
# When you specify arguments other than +:any+ it will try to match the specified arguments in addition
|
23
|
+
# to the method name. It will then also return the first result it can find.
|
24
|
+
def find(method_name, mode=:instance, *args)
|
25
|
+
expectations = mode == :class ? @class_expectations : @instance_expectations
|
26
|
+
candidates = expectations.select { |exp| exp.method_name.to_s.to_sym == method_name.to_s.to_sym }
|
27
|
+
is_single = args.empty? || args.first == :any || (candidates.size == 1 && candidates.first.any_args?)
|
28
|
+
return candidates.first if is_single
|
29
|
+
|
30
|
+
second_pass = candidates.select {|exp| exp.args == args }
|
31
|
+
second_pass.first
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
# contains the syntax for building up an expectation
|
37
|
+
# This is shared between the +ExpecationBuilder+ and the +Expectation+
|
38
|
+
module ExpectationSyntax
|
39
|
+
# tell the expection which arguments it needs to respond to
|
40
|
+
# there is a magic argument here +any+ which configures
|
41
|
+
# the expectation to respond to any arguments
|
42
|
+
def with(*args)
|
43
|
+
@any_args = false unless args.first == :any
|
44
|
+
@args = args
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
# tell the expectation it nees to return this value or the value returned by the block
|
49
|
+
# you provide to this method.
|
50
|
+
def return(value=nil)
|
51
|
+
@return_value = value
|
52
|
+
@return_value ||= yield if block_given?
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
# tell the expectation it needs to raise an error with the specified arguments
|
57
|
+
def raise(*args)
|
58
|
+
@error_args = args
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
# tell the expecation it needs to call the super before the expectation exectution
|
63
|
+
def super_before
|
64
|
+
@super = :before
|
65
|
+
end
|
66
|
+
|
67
|
+
# tell the expectation it needs to call the super after the expecation execution
|
68
|
+
def super_after
|
69
|
+
@super = :after
|
70
|
+
end
|
71
|
+
|
72
|
+
# indicates whether this expectation should match with any arguments
|
73
|
+
# or only for the specified arguments
|
74
|
+
def any_args?
|
75
|
+
@any_args
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# A description of an expectation.
|
80
|
+
# An expectation has the power to call the proxy method or completely ignore it
|
81
|
+
class Expectation
|
82
|
+
|
83
|
+
include ExpectationSyntax
|
84
|
+
|
85
|
+
# gets the method_name to which this expectation needs to listen to
|
86
|
+
attr_reader :method_name
|
87
|
+
|
88
|
+
# the arguments that this expectation needs to be constrained by
|
89
|
+
attr_reader :args
|
90
|
+
|
91
|
+
# the error_args that this expectation will raise an error with
|
92
|
+
attr_reader :error_args
|
93
|
+
|
94
|
+
# the value that this expecation will return when executed
|
95
|
+
attr_reader :return_value
|
96
|
+
|
97
|
+
# indicator for the mode to call the super +:before+, +:after+ and +nil+
|
98
|
+
attr_reader :super
|
99
|
+
|
100
|
+
# Initializes a new instance of an expectation
|
101
|
+
def initialize(method_name, args, error_args, return_value, super_mode)
|
102
|
+
@method_name, @args, @error_args, @return_value, @super =
|
103
|
+
method_name, args, error_args, return_value, super_mode
|
104
|
+
@any_args = true
|
105
|
+
end
|
106
|
+
|
107
|
+
# indicates whether this expecation will raise an event.
|
108
|
+
def has_error_args?
|
109
|
+
!@error_args.nil?
|
110
|
+
end
|
111
|
+
|
112
|
+
# indicates whether this expectation will return a value.
|
113
|
+
def has_return_value?
|
114
|
+
!@return_value.nil?
|
115
|
+
end
|
116
|
+
|
117
|
+
# call the super before the expectation
|
118
|
+
def super_before?
|
119
|
+
@super == :before
|
120
|
+
end
|
121
|
+
|
122
|
+
# indicates whether super needs to be called somewhere
|
123
|
+
def call_super?
|
124
|
+
!@super.nil?
|
125
|
+
end
|
126
|
+
|
127
|
+
# executes this expectation with its configuration
|
128
|
+
def execute(*margs)
|
129
|
+
ags = any_args? ? (margs.empty? ? :any : margs) : args
|
130
|
+
raise *@error_args if has_error_args?
|
131
|
+
return return_value if has_return_value?
|
132
|
+
nil
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Constructs the expecation object.
|
137
|
+
# Used as a boundary between the definition and usage of the expectation
|
138
|
+
class ExpectationBuilder
|
139
|
+
|
140
|
+
include ExpectationSyntax
|
141
|
+
|
142
|
+
# initialises a new instance of the expectation builder
|
143
|
+
# this builder is passed into the block to allow only certain
|
144
|
+
# operations in the block.
|
145
|
+
def initialize(method_name)
|
146
|
+
@method_name, @return_value, @super, @block, @error_args, @args, @any_args =
|
147
|
+
method_name, nil, nil, nil, nil, [], true
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
|
152
|
+
# build up the expectation with the provided arguments
|
153
|
+
def build
|
154
|
+
Expectation.new @method_name, @args, @error_args, @return_value, @super
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/isolator'
|
2
|
+
require File.dirname(__FILE__) + '/method_call_recorder'
|
3
|
+
require File.dirname(__FILE__) + '/expectation'
|
4
|
+
require File.dirname(__FILE__) + '/verification'
|
5
|
+
|
6
|
+
# Caricature - Bringing simple mocking to the DLR
|
7
|
+
# ===============================================
|
8
|
+
#
|
9
|
+
# This project aims to make interop between IronRuby objects and .NET objects easier.
|
10
|
+
# The idea is that it integrates nicely with bacon and later rspec and that it transparently lets you mock ironruby ojbects
|
11
|
+
# as well as CLR objects/interfaces.
|
12
|
+
# Caricature handles interfaces, interface inheritance, CLR objects, CLR object instances, Ruby classes and instances of Ruby classes.
|
13
|
+
#
|
14
|
+
# From the start I wanted to do away with names like mock, stub, record, replay, verify etc.
|
15
|
+
# Instead I took the advice from Roy Osherhove and went with a name of Isolation for creating a mock.
|
16
|
+
#
|
17
|
+
# An Isolation will create what in Rhino.Mocks would be called a DynamicMock (but can be a partial too) :)
|
18
|
+
# In Moq it would be the Loose mocking strategy.
|
19
|
+
#
|
20
|
+
# The naming of the methods for creating mocks is the one that JP Boodhoo proposed WhenToldTo and WasToldTo.
|
21
|
+
# To specify a stub/expectation on an isolation you have one and only one way of doing that with the method called when_receiving.
|
22
|
+
# Then only if you're interested in asserting if a method has been called you can use the did_receive? method for this.
|
23
|
+
#
|
24
|
+
#
|
25
|
+
# isolation = Isolation.for(Ninja)
|
26
|
+
# isolation.when_receiving(:attack) do |exp|
|
27
|
+
# exp.with(:shuriken)
|
28
|
+
# exp.return(3)
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# Battle.new(mock)
|
32
|
+
# battle.combat
|
33
|
+
#
|
34
|
+
# isolation.did_receive?(:attack).should.be.true?
|
35
|
+
#
|
36
|
+
#
|
37
|
+
# It may be very important to note that when you're going to be isolating CLR classes to be used in other CLR classes
|
38
|
+
# you still need to obide by the CLR rules. That means if you want to redefine a method you'll need to make sure it's
|
39
|
+
# marked as virtual. Static types are still governed by static type rules. I'm working on a solution around those
|
40
|
+
# problems but for the time being this is how it has to be.
|
41
|
+
module Caricature
|
42
|
+
|
43
|
+
# The context for an isolator. This contains the +subject+, +recorder+, +expectations+ and +messenger+
|
44
|
+
IsolatorContext = Struct.new(:subject, :recorder, :expectations, :messenger)
|
45
|
+
|
46
|
+
# Instead of using confusing terms like Mocking and Stubbing which is basically the same. Caricature tries
|
47
|
+
# to unify those concepts by using a term of *Isolation*.
|
48
|
+
# When you're testing you typically want to be in control of what you're testing. To do that you isolate
|
49
|
+
# dependencies and possibly define return values for methods on them.
|
50
|
+
# At a later stage you might be interested in which method was called, maybe even with which parameters
|
51
|
+
# or you might be interested in the amount of times it has been called.
|
52
|
+
class Isolation
|
53
|
+
|
54
|
+
# the real instance of the isolated subject
|
55
|
+
# used to forward calls in partial mocks
|
56
|
+
attr_reader :instance
|
57
|
+
|
58
|
+
# the method call recorder
|
59
|
+
attr_reader :recorder
|
60
|
+
|
61
|
+
# the expecations set for this isolation
|
62
|
+
attr_reader :expectations
|
63
|
+
|
64
|
+
# Initializes a new instance of this isolation.
|
65
|
+
def initialize(isolator, context)
|
66
|
+
@instance = isolator.subject
|
67
|
+
@recorder = context.recorder
|
68
|
+
@messenger = context.messenger
|
69
|
+
@expectations = context.expectations
|
70
|
+
isolator.isolation.class.instance_variable_set("@___context___", self)
|
71
|
+
end
|
72
|
+
|
73
|
+
# record and send the message to the isolation.
|
74
|
+
# takes care of following expectations rules when sending messages.
|
75
|
+
def send_message(method_name, return_type, *args, &b)
|
76
|
+
recorder.record_call method_name, :instance, *args, &b
|
77
|
+
@messenger.deliver(method_name, return_type, *args, &b)
|
78
|
+
end
|
79
|
+
|
80
|
+
# record and send the message to the isolation.
|
81
|
+
# takes care of following expectations rules when sending messages.
|
82
|
+
def send_class_message(method_name, return_type, *args, &b)
|
83
|
+
recorder.record_call method_name, :class, *args, &b
|
84
|
+
@messenger.deliver_to_class(method_name, return_type, *args, &b)
|
85
|
+
end
|
86
|
+
|
87
|
+
# builds up an expectation for an instance method, allows for overriding the result returned by the method
|
88
|
+
def create_override(method_name, &block)
|
89
|
+
internal_create_override method_name, :instance, &block
|
90
|
+
end
|
91
|
+
|
92
|
+
# builds up an expectation for a class method, allows for overriding the result returned by the class method
|
93
|
+
def create_class_override(method_name, &block)
|
94
|
+
puts "creating override"
|
95
|
+
internal_create_override method_name, :class, &block
|
96
|
+
end
|
97
|
+
|
98
|
+
# asserts whether the method has been called for the specified configuration
|
99
|
+
def verify(method_name, &block)
|
100
|
+
internal_verify method_name, :instance, &block
|
101
|
+
end
|
102
|
+
|
103
|
+
# asserts whether the method has been called for the specified configuration
|
104
|
+
def class_verify(method_name, &block)
|
105
|
+
internal_verify method_name, :class, &block
|
106
|
+
end
|
107
|
+
|
108
|
+
class << self
|
109
|
+
|
110
|
+
# Creates an isolation object complete with proxy and method call recorder
|
111
|
+
# It works out which isolation it needs to create and provide and initializes the
|
112
|
+
# method call recorder
|
113
|
+
def for(subject, recorder = MethodCallRecorder.new, expectations = Expectations.new)
|
114
|
+
context = IsolatorContext.new subject, recorder, expectations
|
115
|
+
|
116
|
+
isolator = RubyIsolator.for context
|
117
|
+
isolation = new(isolator, context)
|
118
|
+
isolator.isolation
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
protected
|
124
|
+
|
125
|
+
def internal_create_override(method_name, mode=:instance, &block)
|
126
|
+
builder = ExpectationBuilder.new method_name
|
127
|
+
block.call builder unless block.nil?
|
128
|
+
exp = builder.build
|
129
|
+
expectations.add_expectation exp, mode
|
130
|
+
exp
|
131
|
+
end
|
132
|
+
|
133
|
+
def internal_verify(method_name, mode=:instance, &block)
|
134
|
+
verification = Verification.new(method_name, recorder, mode)
|
135
|
+
block.call verification unless block.nil?
|
136
|
+
verification
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# +Mock+ is a synonym for +Isolation+ so you can still use it if you're that way inclined
|
141
|
+
Mock = Isolation unless defined? Mock
|
142
|
+
|
143
|
+
# +Stub+ is a synonym for +Isolation+ so you can still use it if you're that way inclined
|
144
|
+
Stub = Isolation unless defined? Stub
|
145
|
+
|
146
|
+
end
|