File: spy.rb

package info (click to toggle)
ruby-spy 1.0.1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 360 kB
  • sloc: ruby: 3,101; makefile: 2
file content (196 lines) | stat: -rw-r--r-- 6,047 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
require "spy/exceptions"
require "spy/core_ext/marshal"
require "spy/agency"
require "spy/api"
require "spy/base"
require "spy/call_log"
require "spy/constant"
require "spy/mock"
require "spy/nest"
require "spy/subroutine"
require "spy/version"

module Spy

  class << self
    # create a spy on given object
    # @param base_object
    # @param method_names *[Hash,Symbol] will spy on these methods and also set default return values
    # @return [Subroutine, Array<Subroutine>]
    def on(base_object, *method_names)
      spies = method_names.map do |method_name|
        create_and_hook_spy(base_object, method_name)
      end.flatten

      spies.size > 1 ? spies : spies.first
    end

    # removes the spy from the from the given object
    # @param base_object
    # @param method_names *[Symbol]
    # @return [Subroutine, Array<Subroutine>]
    def off(base_object, *method_names)
      removed_spies = method_names.map do |method_name|
        spy = Subroutine.get(base_object, method_name)
        if spy
          spy.unhook
        end
      end

      removed_spies.size > 1 ? removed_spies : removed_spies.first
    end

    # stubs the instance method of a given Class so all instance methods of this
    # class will have the given method stubbed
    # @param base_class [Class] The class you wish to stub the instance methods of
    # @param method_names *[Symbol, Hash]
    # @return [Spy,Array<Spy>]
    def on_instance_method(base_class, *method_names)
      spies = method_names.map do |method_name|
        create_and_hook_spy(base_class, method_name, false)
      end.flatten

      spies.size > 1 ? spies : spies.first
    end

    # remove the stub from given Class
    # @param base_class [Class]
    # @param method_names *[Symbol]
    # @return [Spy]
    def off_instance_method(base_class, *method_names)
      removed_spies = method_names.map do |method_name|
        spy = Subroutine.get(base_class, method_name, false)
        if spy
          spy.unhook
        else
          raise NoSpyError, "#{method_name} was not hooked on #{base_class.inspect}."
        end
      end

      removed_spies.size > 1 ? removed_spies : removed_spies.first
    end

    # create a stub for constants on given module
    # @param base_module [Module]
    # @param constant_names *[Symbol, Hash]
    # @return [Constant, Array<Constant>]
    def on_const(base_module, *constant_names)
      if base_module.is_a?(Hash) || base_module.is_a?(Symbol)
        constant_names.unshift(base_module)
        base_module = Object
      end
      spies = constant_names.map do |constant_name|
        case constant_name
        when Symbol
          Constant.on(base_module, constant_name)
        when Hash
          constant_name.map do |name, result|
            Constant.on(base_module, name).and_return(result)
          end
        else
          raise ArgumentError, "#{constant_name.class} is an invalid input, #on only accepts Symbol, and Hash"
        end
      end.flatten

      spies.size > 1 ? spies : spies.first
    end

    # removes stubs from given module
    # @param base_module [Module]
    # @param constant_names *[Symbol]
    # @return [Constant, Array<Constant>]
    def off_const(base_module, *constant_names)
      if base_module.is_a?(Symbol)
        constant_names.unshift(base_module)
        base_module = Object
      end

      spies = constant_names.map do |constant_name|
        unless constant_name.is_a?(Symbol)
          raise ArgumentError, "#{constant_name.class} is an invalid input, #on only accepts Symbol, and Hash"
        end
        Constant.off(base_module, constant_name)
      end

      spies.size > 1 ? spies : spies.first
    end

    # Create a mock object from a given class
    # @param klass [Class] class you wish to mock
    # @param stubs *[Symbol, Hash] methods you with to stub
    # @return [Object]
    def mock(klass, *stubs)
      new_mock = Mock.new(klass).new
      if stubs.size > 0
        on(new_mock, *stubs)
      end
      new_mock
    end

    # create a mock object from a given class with all the methods stubbed out
    # and returning nil unless specified otherwise.
    # @param klass [Class] class you wish to mock
    # @param stubs *[Symbol, Hash] methods you with to stub
    # @return [Object]
    def mock_all(klass, *stubs)
      mock_klass = Mock.new(klass)
      new_mock = mock_klass.new

      spies = stubs.size > 0 ? on(new_mock, *stubs) : []

      unstubbed_methods = mock_klass.mocked_methods - spies.map(&:method_name)
      on(new_mock, *unstubbed_methods) if unstubbed_methods.size > 0

      new_mock
    end

    # unhook all methods
    def teardown
      Agency.instance.dissolve!
    end

    # retrieve the spy from an object
    # @param base_object
    # @param method_names *[Symbol]
    # @return [Subroutine, Array<Subroutine>]
    def get(base_object, *method_names)
      spies = method_names.map do |method_name|
        Subroutine.get(base_object, method_name)
      end

      spies.size > 1 ? spies : spies.first
    end

    # retrieve the constant spies from an object
    # @param base_module
    # @param constant_names *[Symbol]
    # @return [Constant, Array<Constant>]
    def get_const(base_module, *constant_names)
      if base_module.is_a?(Symbol)
        constant_names.unshift(base_module)
        base_module = Object
      end

      spies = constant_names.map do |constant_name|
        Constant.get(base_module, constant_name)
      end

      spies.size > 1 ? spies : spies.first
    end

    private

    def create_and_hook_spy(base_object, method_name, singleton_method = true)
      case method_name
      when String, Symbol
        Subroutine.on(base_object, method_name, singleton_method)
      when Hash
        method_name.map do |name, result|
          Subroutine.on(base_object, name, singleton_method).and_return(result)
        end
      else
        raise ArgumentError, "#{method_name.class} is an invalid input, #on only accepts String, Symbol, and Hash"
      end
    end
  end
end