Class: MockProxy
- Inherits:
-
Object
- Object
- MockProxy
- Defined in:
- lib/mock_proxy.rb,
lib/mock_proxy/version.rb
Overview
A non-opinionated proxy object that has multiple uses. It can be used for mocking, spying, stubbing. Use as a dummy, double, fake, etc. Every test double type possible. How? Let’s see
Example, say you want to stub this scenario: Model.new.generate_email.validate!.send(to: email) That would have be 5-6 lines of stubbing. If this sounds like stub_chain, you’re on the right track. It was removed in RSpec 3 (or 2?). It’s similar to that but it does things differently First, it doesn’t require you to use it in a stub Second, it’s use of callbacks means you can define anything, a stub or a mock (expectation) or a spy or whatever you want
To use MockProxy, initialize it with a hash. Each key is a method call. Each call either returns a new proxy or calls the callback. If the value is a callback, it calls it immediately with the args and block. If the value is a hash, it returns a new proxy with the value as the hash. MockProxy will warn if you don’t use hashes or callbacks and will also warn if you did not define all the method calls (it won’t automatically return itself for methods not defined in the hash)
Example use:
let(:model_proxy) { MockProxy.new(receive_email: callback {}, generate_email: { validate!: { send: callback { |to| email } } }) }
before { allow(Model).to receive(:new).and_return model_proxy }
# ...
describe 'Model' do
it 'model also receives email' do
MockProxy.observe(model_proxy, :receive_email) do ||
expect().to eq 'message'
end
run_system_under_test
end
end
NOTE: You don’t have to use only one mock proxy for all calls. You can break it up if you want to have more control over each method call
Example:
let(:model_proxy) do
callback = callback do |type|
MockProxy.merge(generator_proxy, decorate: callback { |*args| method_call(type, *args) })
generator_proxy
end
MockProxy.new(generate_email: callback)
end
let(:generator_proxy) { MockProxy.new(validate!: { send: callback { |to| email } }) }
Constant Summary collapse
- VERSION =
The version number
'0.4.6'
Class Method Summary collapse
-
.get(proxy, key_path) ⇒ Block
Retrieve the existing callback or callback tree at the specified key path.
-
.merge(proxy, new_callback_hash) ⇒ MockProxy
Deep merges the callback tree, replacing existing values with new values.
-
.observe(proxy, key_path) {|args| ... } ⇒ MockProxy
Add an observer to an existing proxy.
-
.replace_at(proxy, key_path, &block) ⇒ MockProxy
Replaces the callback at the specified key path, but only if there was one there before.
-
.set_at(proxy, key_path, &block) ⇒ MockProxy
Sets the callback at the specified key path, regardless if there was a callback there before.
-
.wrap(proxy, key_path) {|args,| ... } ⇒ MockProxy
Wraps the existing callback with your block.
Instance Method Summary collapse
-
#initialize(callback_hash) ⇒ MockProxy
constructor
A new instance of MockProxy.
- #method_missing(name, *args, &block) ⇒ Object
Constructor Details
#initialize(callback_hash) ⇒ MockProxy
Returns a new instance of MockProxy.
262 263 264 265 266 267 |
# File 'lib/mock_proxy.rb', line 262 def initialize(callback_hash) unless self.class.send(:valid_callback_tree?, callback_hash) fail "Not a valid callback tree: #{callback_hash}" end @callback_hash = callback_hash.deep_stringify_keys.freeze end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(name, *args, &block) ⇒ Object
270 271 272 273 274 275 276 277 278 279 280 |
# File 'lib/mock_proxy.rb', line 270 def method_missing(name, *args, &block) current = @callback_hash[name.to_s] if MockProxy.send(:valid_callback?, current) current.call(*args, &block) else if !current fail "Missing method #{name}. Please add this definition to your mock proxy" end MockProxy.new(current.freeze) end end |
Class Method Details
.get(proxy, key_path) ⇒ Block
Retrieve the existing callback or callback tree at the specified key path
NOTE: We freeze the hash so you cannot modify it
Use case: Retrieve callback to mock
60 61 62 |
# File 'lib/mock_proxy.rb', line 60 def self.get(proxy, key_path) get_and_validate_callback(proxy, key_path) end |
.merge(proxy, new_callback_hash) ⇒ MockProxy
Deep merges the callback tree, replacing existing values with new values. Avoid using this method for one method change; prefer replace_at. It has clearer intent and less chances to mess up. MockProxy.merge uses deep_merge under the hood and can have unexpected behaviour. It also does not type check. Use at risk
Use case: Reuse existing stub but with some different values
74 75 76 77 78 79 80 |
# File 'lib/mock_proxy.rb', line 74 def self.merge(proxy, new_callback_hash) existing_callback_hash = proxy.instance_variable_get('@callback_hash') new_callback_hash = new_callback_hash.deep_stringify_keys new_callback_hash = existing_callback_hash.deep_merge(new_callback_hash).freeze proxy.instance_variable_set('@callback_hash', new_callback_hash) proxy end |
.observe(proxy, key_path) {|args| ... } ⇒ MockProxy
Add an observer to an existing proxy
Use case: Observe method call without changing the existing callback’s stubbed return value
122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/mock_proxy.rb', line 122 def self.observe(proxy, key_path, &block) callback = get_and_validate_callback(proxy, key_path) # Wrap existing callback, calling the provided block before it # Multiple calls to .observe will create a pyramid of callbacks, calling the observers before # eventually calling the existing callback new_callback = callback do |*args| block.call(*args) callback.call(*args) end set_callback(proxy, key_path, new_callback) proxy end |
.replace_at(proxy, key_path, &block) ⇒ MockProxy
Replaces the callback at the specified key path, but only if there was one there before. Without creating new paths comes validation, including checking that this replaces an existing callback, sort of like mkdir (without the -p option)
Use case: Replace existing stub with a new callback without creating new method chains
92 93 94 95 |
# File 'lib/mock_proxy.rb', line 92 def self.replace_at(proxy, key_path, &block) set_callback(proxy, key_path, block) proxy end |
.set_at(proxy, key_path, &block) ⇒ MockProxy
Sets the callback at the specified key path, regardless if there was a callback there before. No validation comes with automatic path creation, meaning the key path will be defined it it hasn’t already, sort of like mkdir -p
Use case: Sets a new stub at specified key path while creating new method chains
107 108 109 110 |
# File 'lib/mock_proxy.rb', line 107 def self.set_at(proxy, key_path, &block) set_callback(proxy, key_path, block, false) proxy end |
.wrap(proxy, key_path) {|args,| ... } ⇒ MockProxy
Wraps the existing callback with your block
Use case: Get full control of the existing callback while running custom code
145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/mock_proxy.rb', line 145 def self.wrap(proxy, key_path, &block) callback = get_and_validate_callback(proxy, key_path) # Wrap existing callback, calling the provided block before it # Multiple calls to .observe will create a pyramid of callbacks, calling the observers before # eventually calling the existing callback new_callback = callback do |*args| block.call(*args, &callback) end set_callback(proxy, key_path, new_callback) proxy end |