Class: MockProxy

Inherits:
Object
  • Object
show all
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 |message|
      expect(message).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 } }) }

Author:

  • Geoff Lee

Since:

  • 0.1.0

Constant Summary collapse

VERSION =

The version number

Since:

  • 0.1.0

'0.4.6'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(callback_hash) ⇒ MockProxy

Returns a new instance of MockProxy.

Parameters:

  • callback_hash (Hash)

    the tree of chained method calls

Since:

  • 0.1.0



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

Since:

  • 0.1.0



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

Parameters:

  • proxy (MockProxy)

    existing proxy

  • key_path (String, Symbol, #to_s, Array<String, Symbol, #to_s>)

    the chain of methods or key path. Can be a dot delimited key path or an array of method names as strings or symbols

Returns:

  • (Block)

Since:

  • 0.1.0



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

Parameters:

  • proxy (MockProxy)

    existing proxy

  • new_callback_hash (Hash)

    new partial callback tree

Returns:

Since:

  • 0.1.0



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

Parameters:

  • proxy (MockProxy)

    existing proxy

  • key_path (String, Symbol, #to_s, Array<String, Symbol, #to_s>)

    the chain of methods or key path. Can be a dot delimited key path or an array of method names as strings or symbols

Yield Parameters:

  • args (*args)

Yield Returns:

  • (optional)

Returns:

Since:

  • 0.1.0



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

Parameters:

  • proxy (MockProxy)

    existing proxy

  • key_path (String, Symbol, #to_s, Array<String, Symbol, #to_s>)

    the chain of methods or key path. Can be a dot delimited key path or an array of method names as strings or symbols

Returns:

Since:

  • 0.1.0



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

Parameters:

  • proxy (MockProxy)

    existing proxy

  • key_path (String, Symbol, #to_s, Array<String, Symbol, #to_s>)

    the chain of methods or key path. Can be a dot delimited key path or an array of method names as strings or symbols

Returns:

Since:

  • 0.1.0



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

Parameters:

  • proxy (MockProxy)

    existing proxy

  • key_path (String, Symbol, #to_s, Array<String, Symbol, #to_s>)

    the chain of methods or key path. Can be a dot delimited key path or an array of method names as strings or symbols

Yield Parameters:

  • args, (*args, &block)

    original callback

Yield Returns:

  • (optional)

Returns:

Since:

  • 0.1.0



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