spy 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.travis.yml +5 -0
  2. data/Gemfile +1 -0
  3. data/README.md +22 -7
  4. data/Rakefile +2 -0
  5. data/lib/spy.rb +39 -6
  6. data/lib/spy/agency.rb +42 -27
  7. data/lib/spy/call_log.rb +26 -0
  8. data/lib/spy/constant.rb +72 -14
  9. data/lib/spy/core_ext/marshal.rb +1 -0
  10. data/lib/spy/double.rb +17 -0
  11. data/lib/spy/nest.rb +27 -0
  12. data/lib/spy/subroutine.rb +146 -44
  13. data/lib/spy/version.rb +1 -1
  14. data/spec/spy/any_instance_spec.rb +518 -0
  15. data/spec/spy/mock_spec.rb +46 -554
  16. data/spec/spy/mutate_const_spec.rb +21 -63
  17. data/spec/spy/null_object_mock_spec.rb +11 -39
  18. data/spec/spy/partial_mock_spec.rb +3 -62
  19. data/spec/spy/stash_spec.rb +30 -37
  20. data/spec/spy/stub_spec.rb +0 -6
  21. data/spec/spy/to_ary_spec.rb +5 -5
  22. data/test/integration/test_constant_spying.rb +1 -1
  23. data/test/integration/test_instance_method.rb +32 -0
  24. data/test/integration/test_subroutine_spying.rb +7 -4
  25. data/test/spy/test_double.rb +4 -0
  26. data/test/spy/test_subroutine.rb +28 -3
  27. data/test/support/pen.rb +15 -0
  28. metadata +8 -30
  29. data/spec/spy/bug_report_10260_spec.rb +0 -8
  30. data/spec/spy/bug_report_10263_spec.rb +0 -24
  31. data/spec/spy/bug_report_496_spec.rb +0 -18
  32. data/spec/spy/bug_report_600_spec.rb +0 -24
  33. data/spec/spy/bug_report_7611_spec.rb +0 -16
  34. data/spec/spy/bug_report_8165_spec.rb +0 -31
  35. data/spec/spy/bug_report_830_spec.rb +0 -21
  36. data/spec/spy/bug_report_957_spec.rb +0 -22
  37. data/spec/spy/double_spec.rb +0 -12
  38. data/spec/spy/failing_argument_matchers_spec.rb +0 -94
  39. data/spec/spy/options_hash_spec.rb +0 -35
  40. data/spec/spy/precise_counts_spec.rb +0 -68
  41. data/spec/spy/stubbed_message_expectations_spec.rb +0 -47
  42. data/spec/spy/test_double_spec.rb +0 -54
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.3"
4
+ - jruby-19mode
5
+ - rbx-19mode
data/Gemfile CHANGED
@@ -6,3 +6,4 @@ gem 'pry'
6
6
  gem 'pry-nav'
7
7
  gem 'yard'
8
8
  gem 'redcarpet'
9
+ gem 'rake'
data/README.md CHANGED
@@ -29,11 +29,14 @@ Fail faster, code faster.
29
29
 
30
30
  ## Why not to use this
31
31
 
32
- * Api is not stable
33
- * missing these features
34
- * Mocking null objects
35
- * argument matchers for Spy::Method#has\_been\_called\_with
36
- * watch all calls to an object to check order in which they are called
32
+ * mocking null objects is not supported(yet)
33
+ * no argument matchers for Spy::Method#has\_been\_called\_with
34
+ * cannot watch all calls to an object to check order in which they are called
35
+ * cannot transfer nested constants when stubbing a constant
36
+ * i don't think anybody uses this anyway
37
+ * nobody on github does
38
+ * #with is not supported yet
39
+ * this is probably a code smell. You either need to abstract your method more or add separate tests.
37
40
 
38
41
  ## Installation
39
42
 
@@ -56,10 +59,13 @@ Or install it yourself as:
56
59
  A method stub overrides a pre-existing method and records all calls to specified method. You can set the spy to return either the original method or your own custom implementation.
57
60
 
58
61
  Spy support 2 different ways of spying an existing method on an object.
62
+
59
63
  ```ruby
60
64
  Spy.on(book, title: "East of Eden")
61
65
  Spy.on(book, :title).and_return("East of Eden")
62
66
  Spy.on(book, :title).and_return { "East of Eden" }
67
+
68
+ book.title #=> "East of Eden"
63
69
  ```
64
70
 
65
71
  Spy will raise an error if you try to stub on a method that doesn't exist.
@@ -69,6 +75,15 @@ You can force the creation of a sstub on method that didn't exist but it really
69
75
  Spy.new(book, :flamethrower).hook(force:true).and_return("burnninante")
70
76
  ```
71
77
 
78
+ You can also stub instance methods of Classes and Modules
79
+
80
+ ```ruby
81
+ Spy.on_instance_method(Book, :title).and_return("Cannery Row")
82
+
83
+ Book.new(title: "Siddhartha").title #=> "Cannery Row"
84
+ Book.new(title: "The Big Cheese").title #=> "Cannery Row"
85
+ ```
86
+
72
87
  ### Test Doubles
73
88
 
74
89
  A test double is an object that stands in for a real object.
@@ -155,7 +170,7 @@ In spec\_helper.rb
155
170
  require "rspec/autorun"
156
171
  require "spy"
157
172
  RSpec.configure do |c|
158
- c.before { Spy.teardown }
173
+ c.after { Spy.teardown }
159
174
  end
160
175
  ```
161
176
 
@@ -164,7 +179,7 @@ end
164
179
  ```ruby
165
180
  require "spy"
166
181
  class Test::Unit::TestCase
167
- def setup
182
+ def teardown
168
183
  Spy.teardown
169
184
  end
170
185
  end
data/Rakefile CHANGED
@@ -6,3 +6,5 @@ Rake::TestTask.new do |t|
6
6
  t.test_files = FileList['test/**/test*.rb']
7
7
  t.verbose = true
8
8
  end
9
+
10
+ task :default => [:test]
data/lib/spy.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "spy/core_ext/marshal"
2
2
  require "spy/agency"
3
+ require "spy/call_log"
3
4
  require "spy/constant"
4
5
  require "spy/double"
5
6
  require "spy/nest"
@@ -7,7 +8,7 @@ require "spy/subroutine"
7
8
  require "spy/version"
8
9
 
9
10
  module Spy
10
- SECRET_SPY_KEY = Object.new
11
+
11
12
  class << self
12
13
  # create a spy on given object
13
14
  # @param base_object
@@ -38,12 +39,34 @@ module Spy
38
39
  removed_spies.size > 1 ? removed_spies : removed_spies.first
39
40
  end
40
41
 
42
+ #
43
+ def on_instance_method(base_class, *method_names)
44
+ spies = method_names.map do |method_name|
45
+ create_and_hook_spy(base_class, method_name, false)
46
+ end.flatten
47
+
48
+ spies.size > 1 ? spies : spies.first
49
+ end
50
+
51
+ def off_instance_method(base_object, *method_names)
52
+ removed_spies = method_names.map do |method_name|
53
+ spy = Subroutine.get(base_object, method_name, false)
54
+ if spy
55
+ spy.unhook
56
+ else
57
+ raise "Spy was not found"
58
+ end
59
+ end
60
+
61
+ removed_spies.size > 1 ? removed_spies : removed_spies.first
62
+ end
63
+
41
64
  # create a stub for constants on given module
42
65
  # @param base_module [Module]
43
66
  # @param constant_names *[Symbol, Hash]
44
67
  # @return [Constant, Array<Constant>]
45
68
  def on_const(base_module, *constant_names)
46
- if base_module.is_a? Symbol
69
+ if base_module.is_a?(Hash) || base_module.is_a?(Symbol)
47
70
  constant_names.unshift(base_module)
48
71
  base_module = Object
49
72
  end
@@ -68,6 +91,11 @@ module Spy
68
91
  # @param constant_names *[Symbol]
69
92
  # @return [Constant, Array<Constant>]
70
93
  def off_const(base_module, *constant_names)
94
+ if base_module.is_a?(Hash) || base_module.is_a?(Symbol)
95
+ constant_names.unshift(base_module)
96
+ base_module = Object
97
+ end
98
+
71
99
  spies = constant_names.map do |constant_name|
72
100
  case constant_name
73
101
  when String, Symbol
@@ -112,7 +140,12 @@ module Spy
112
140
  # @param constant_names *[Symbol]
113
141
  # @return [Constant, Array<Constant>]
114
142
  def get_const(base_module, *constant_names)
115
- spies = constant_names.map do |method_name|
143
+ if base_module.is_a?(Hash) || base_module.is_a?(Symbol)
144
+ constant_names.unshift(base_module)
145
+ base_module = Object
146
+ end
147
+
148
+ spies = constant_names.map do |constant_name|
116
149
  Constant.get(base_module, constant_name)
117
150
  end
118
151
 
@@ -121,13 +154,13 @@ module Spy
121
154
 
122
155
  private
123
156
 
124
- def create_and_hook_spy(base_object, method_name, opts = {})
157
+ def create_and_hook_spy(base_object, method_name, singleton_method = true, hook_opts = {})
125
158
  case method_name
126
159
  when String, Symbol
127
- Subroutine.new(base_object, method_name).hook(opts)
160
+ Subroutine.new(base_object, method_name, singleton_method).hook(hook_opts)
128
161
  when Hash
129
162
  method_name.map do |name, result|
130
- create_and_hook_spy(base_object, name, opts).and_return(result)
163
+ create_and_hook_spy(base_object, name, singleton_method, hook_opts).and_return(result)
131
164
  end
132
165
  else
133
166
  raise ArgumentError.new "#{method_name.class} is an invalid input, #on only accepts String, Symbol, and Hash"
@@ -1,65 +1,80 @@
1
1
  require 'singleton'
2
2
 
3
3
  module Spy
4
+ # Manages all the spies
4
5
  class Agency
5
6
  include Singleton
6
7
 
7
- attr_reader :subroutines, :constants, :doubles
8
-
8
+ # @private
9
9
  def initialize
10
10
  clear!
11
11
  end
12
12
 
13
+
14
+ # given a spy ID it will return the associated spy
15
+ # @param id [Integer] spy object id
16
+ # @return [Nil, Subroutine, Constant, Double]
17
+ def find(id)
18
+ @spies[id]
19
+ end
20
+
21
+ # Record that a spy was initialized and hooked
22
+ # @param spy [Subroutine, Constant, Double]
23
+ # @return [spy]
13
24
  def recruit(spy)
14
25
  case spy
15
- when Subroutine
16
- subroutines << spy
17
- when Constant
18
- constants << spy
19
- when Double
20
- doubles << spy
26
+ when Subroutine, Constant, Double
27
+ @spies[spy.object_id] = spy
21
28
  else
22
29
  raise "Not a spy"
23
30
  end
24
- spy
25
31
  end
26
32
 
33
+ # remove spy from the records
34
+ # @param spy [Subroutine, Constant, Double]
35
+ # @return [spy]
27
36
  def retire(spy)
28
37
  case spy
29
- when Subroutine
30
- subroutines.delete(spy)
31
- when Constant
32
- constants.delete(spy)
33
- when Double
34
- doubles.delete(spy)
38
+ when Subroutine, Constant, Double
39
+ @spies.delete(spy.object_id)
35
40
  else
36
41
  raise "Not a spy"
37
42
  end
38
- spy
39
43
  end
40
44
 
45
+ # checks to see if a spy is hooked
46
+ # @param spy [Subroutine, Constant, Double]
47
+ # @return [Boolean]
41
48
  def active?(spy)
42
49
  case spy
43
- when Subroutine
44
- subroutines.include?(spy)
45
- when Constant
46
- constants.include?(spy)
47
- when Double
48
- doubles.include?(spy)
50
+ when Subroutine, Constant, Double
51
+ @spies.has_key?(spy.object_id)
52
+ else
53
+ raise "Not a spy"
49
54
  end
50
55
  end
51
56
 
57
+ # unhooks all spies and clears records
58
+ # @return [self]
52
59
  def dissolve!
53
- subroutines.each(&:unhook)
54
- constants.each(&:unhook)
60
+ @spies.values.each do |spy|
61
+ spy.unhook if spy.respond_to?(:unhook)
62
+ end
55
63
  clear!
56
64
  end
57
65
 
66
+ # clears records
67
+ # @return [self]
58
68
  def clear!
59
- @subroutines = []
60
- @constants = []
61
- @doubles = []
69
+ @spies = {}
62
70
  self
63
71
  end
72
+
73
+ # returns all the spies that have been initialized since the creation of
74
+ # this agency
75
+ # @return [Array<Subroutine, Constant, Double>]
76
+ def spies
77
+ @spies.values
78
+ end
64
79
  end
65
80
  end
@@ -0,0 +1,26 @@
1
+ module Spy
2
+ class CallLog
3
+
4
+ # @!attribute [r] object
5
+ # @return [Object] object that the method was called from
6
+ #
7
+ # @!attribute [r] called_from
8
+ # @return [String] where the method was called from
9
+ #
10
+ # @!attribute [r] args
11
+ # @return [Array] arguments were sent to the method
12
+ #
13
+ # @!attribute [r] block
14
+ # @return [Proc] the block that was sent to the method
15
+ #
16
+ # @!attribute [r] result
17
+ # @return The result of the method of being stubbed, or called through
18
+
19
+
20
+ attr_reader :object, :called_from, :args, :block, :result
21
+
22
+ def initialize(object, called_from, args, block, result)
23
+ @object, @called_from, @args, @block, @result = object, called_from, args, block, result
24
+ end
25
+ end
26
+ end
@@ -1,66 +1,124 @@
1
1
  module Spy
2
2
  class Constant
3
- attr_reader :base_module, :constant_name, :original_value, :new_value
4
3
 
4
+ # @!attribute [r] base_module
5
+ # @return [Module] the module that is being watched
6
+ #
7
+ # @!attribute [r] constant_name
8
+ # @return [Symbol] the name of the constant that is/will be stubbed
9
+ #
10
+ # @!attribute [r] original_value
11
+ # @return [Object] the original value that was set when it was hooked
12
+
13
+
14
+ attr_reader :base_module, :constant_name, :original_value
15
+
16
+ # @param base_module [Module] the module this spy should be on
17
+ # @param constant_name [Symbol] the constant this spy is watching
5
18
  def initialize(base_module, constant_name)
6
19
  raise "#{base_module.inspect} is not a kind of Module" unless base_module.is_a? Module
7
20
  raise "#{constant_name.inspect} is not a kind of Symbol" unless constant_name.is_a? Symbol
8
21
  @base_module, @constant_name = base_module, constant_name.to_sym
9
22
  @original_value = nil
10
23
  @new_value = nil
11
- @was_defined = nil
24
+ @previously_defined = nil
25
+ end
26
+
27
+ # full name of spied constant
28
+ def name
29
+ "#{base_module.name}::#{constant_name}"
12
30
  end
13
31
 
32
+ # stashes the original constant then overwrites it with nil
33
+ # @param opts [Hash{force => false}] set :force => true if you want it to ignore if the constant exists
34
+ # @return [self]
14
35
  def hook(opts = {})
36
+ Nest.fetch(base_module).add(self)
37
+ Agency.instance.recruit(self)
15
38
  opts[:force] ||= false
16
- @was_defined = base_module.const_defined?(constant_name, false)
17
- if @was_defined || !opts[:force]
39
+ @previously_defined = currently_defined?
40
+ if @previously_defined || !opts[:force]
18
41
  @original_value = base_module.const_get(constant_name, false)
19
42
  end
20
43
  and_return(@new_value)
21
- Nest.fetch(base_module).add(self)
22
- Agency.instance.recruit(self)
23
44
  self
24
45
  end
25
46
 
47
+ # restores the original value of the constant or unsets it if it was unset
48
+ # @return [self]
26
49
  def unhook
27
- if @was_defined
50
+ Nest.get(base_module).remove(self)
51
+ Agency.instance.retire(self)
52
+
53
+ if @previously_defined
28
54
  and_return(@original_value)
29
55
  end
30
56
  @original_value = nil
31
-
32
- Agency.instance.retire(self)
33
- Nest.fetch(base_module).remove(self)
34
57
  self
35
58
  end
36
59
 
60
+ # unsets the constant
61
+ # @return [self]
37
62
  def and_hide
38
- base_module.send(:remove_const, constant_name)
63
+ base_module.send(:remove_const, constant_name) if currently_defined?
39
64
  self
40
65
  end
41
66
 
67
+ # sets the constant to the requested value
68
+ # @param value [Object]
69
+ # @return [self]
42
70
  def and_return(value)
43
71
  @new_value = value
44
- base_module.send(:remove_const, constant_name) if base_module.const_defined?(constant_name, false)
72
+ and_hide
45
73
  base_module.const_set(constant_name, @new_value)
46
74
  self
47
75
  end
48
76
 
77
+ # checks to see if this spy is hooked?
78
+ # @return [Boolean]
49
79
  def hooked?
50
- Nest.get(base_module).hooked?(constant_name)
80
+ self.class.get(base_module, constant_name) == self
81
+ end
82
+
83
+ # checks to see if the constant is hidden?
84
+ # @return [Boolean]
85
+ def hidden?
86
+ hooked? && currently_defined?
87
+ end
88
+
89
+ # checks to see if the constant is currently defined?
90
+ # @return [Boolean]
91
+ def currently_defined?
92
+ base_module.const_defined?(constant_name, false)
93
+ end
94
+
95
+ # checks to see if the constant is previously defined?
96
+ # @return [Boolean]
97
+ def previously_defined?
98
+ @previously_defined
51
99
  end
52
100
 
53
101
  class << self
102
+ # creates a new constant spy and hooks the constant
103
+ # @return [Constant]
54
104
  def on(base_module, constant_name)
55
105
  new(base_module, constant_name).hook
56
106
  end
57
107
 
108
+ # retrieves the spy for given constant and module and unhooks the constant
109
+ # from the module
110
+ # @return [Constant]
58
111
  def off(base_module, constant_name)
59
112
  get(base_module, constant_name).unhook
60
113
  end
61
114
 
115
+ # retrieves the spy for given constnat and module or returns nil
116
+ # @return [Nil, Constant]
62
117
  def get(base_module, constant_name)
63
- Nest.get(base_module).hooked_constants[constant_name]
118
+ nest = Nest.get(base_module)
119
+ if nest
120
+ nest.hooked_constants[constant_name]
121
+ end
64
122
  end
65
123
  end
66
124
  end