Project

spy_rb

0.01
No commit activity in last 3 years
No release in over 3 years
There's a lot of open issues
Mocking frameworks work by stubbing out functionality. Spy works by listening in on functionality and allowing it to run in the background. Spy is designed to be lightweight and work alongside Mocking frameworks instead of trying to replace them entirely.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 0
 Project Readme

Spy

Travis Status Coverage Status Code Climate Gem Version

Transparent Test Spies for Ruby

Description

Mocking frameworks work by stubbing out functionality. Spy works by listening in on functionality and allowing it to run in the background. Spy is designed to be lightweight and work alongside Mocking frameworks instead of trying to replace them entirely.

Why Spy?

  • Less intrusive than mocking
  • Allows you to test message passing without relying on stubbing
  • Great for testing recursive methods or methods with side effects (e.g. test that something is cached and only hits the database once on the intitial call)
  • Works for Ruby 2.x
  • Small and simple
  • Strong test coverage
  • No alias_method pollution
  • No dependencies!

Install

gem install spy_rb

Usage

Spy::API

Spy::API defines the top-level interface for creating spies and for interacting with them globally.

You can use it to create spies in a variety of ways. For these example we'll use the Fruit class because, seriously, who doesn't love fruit:

class Fruit
  def eat(adj)
    puts "you take a bite #{adj}"
  end
end

require 'spy'

# Spy on singleton or bound methods
fruit = Fruit.new
s = Spy.on(fruit, :to_s)
fruit.to_s
s.call_count
#=> 1

s = Spy.on(Fruit, :to_s)
Fruit.to_s
s.call_count
#=> 1

# Spy on instance methods
s = Spy.on_any_instance(Fruit, :to_s)
apple = Fruit.new
apple.to_s
orange = Fruit.new
orange.to_s
s.call_count
#=> 2

# Spied methods respect visibility
Object.private_methods.include?(:fork)
#=> true
Spy.on(Object, :fork)
Object.fork
#=> NoMethodError: private method `fork' called for Object:Class

# Spy will let you know if you're doing something wrong too
Spy.on(Object, :doesnt_exist)
#=> NameError: undefined method `doesnt_exist' for class `Class'

Spy.on(Fruit, :to_s)
=> #<Spy::Instance:0x007feb55affe18 @spied=Fruit, @original=#<Method: Class(Module)#to_s>, @visibility=:public, @conditional_filters=[], @before_callbacks=[], @after_callbacks=[], @around_procs=[], @call_history=[], @strategy=#<Spy::Instance::Strategy::Intercept:0x007feb55affc38 @spy=#<Spy::Instance:0x007feb55affe18 ...>, @intercept_target=#<Class:Fruit>>>

Spy.on(Fruit, :to_s)
#=> Spy::Errors::AlreadySpiedError: Spy::Errors::AlreadySpiedError

# Spy on all of the methods of an object (also see Spy.on_class)
s = Spy.on_object(fruit)
fruit.to_s
s.call_count
#=> 1
s[:to_s].call_count
#=> 1

When you're all finished you'll want to restore your methods to clean up the spies:

# Restore singleton/bound method
s = Spy.on(Object, :to_s)
Spy.restore(Object, :to_s)

# Restore instance method
s = Spy.on_any_instance(Object, :to_s)
Spy.restore(Object, :to_s, :instance_method)

# Restore method_missing-style delegation
Spy.restore(Object, :message, :dynamic_delegation)

# Global restore
s = Spy.on(Object, :to_s)
Spy.restore(:all)

If you're using spy_rb in the context of a test suite, you may want to patch a Spy.restore(:all) into your teardowns:

class ActiveSupport::TestCase
  teardown do
    Spy.restore(:all)
  end
end

Spy::Instance

Once you've created a spy instance, then there are a variety of ways to interact with that spy. See Spy::Instance for the full list of supported methods.

Spy::Instance#call_count will tell you how many times the spied method was called:

fruit = Fruit.new
spy = Spy.on(fruit, :eat)
fruit.eat(:slowly)
spy.call_count
#=> 1

fruit.eat(:quickly)
spy.call_count
#=> 2

Spy::Instance#when lets you specify conditions as to when the spy should track calls:

fruit = Fruit.new
spy = Spy.on(fruit, :eat)
spy.when {|method_call| method_call.args.first == :quickly}
fruit.eat(:slowly)
spy.call_count
#=> 0

fruit.eat(:quickly)
spy.call_count
#=> 1

Spy::Instance#before and Spy::Instance#after give you callbacks for your spy:

fruit = Fruit.new
spy = Spy.on(fruit, :eat)
spy.before { puts 'you wash your hands' }
spy.after { puts 'you rejoice in your triumph' }
fruit.eat(:happily)
#=> you wash your hands
#=> you take a bite happily
#=> you rejoice in your triumph

# #before and #after can both accept arguments just like #when

Spy::Instance#wrap allows you to do so more complex things. Be sure to call the original block though! You don't have to worry about passing args to the original. Those are wrapped up for you; you just need to #call it.

require 'benchmark'
fruit = Fruit.new
spy = Spy.on(fruit, :eat)
spy.wrap do |method_call, &original|
  puts Benchmark.measure { original.call }
end
fruit.eat(:hungrily)
#=> you take a bite hungrily
#=> 0.000000   0.000000   0.000000 (  0.000039)

Spy::Instance#instead lets you emulate stubbing:

fruit = Fruit.new
spy = Spy.on(fruit, :eat)
spy.instead { puts "taking a nap" }
fruit.eat(:hungrily)
#=> taking a nap

Spy::Instance#call_history keeps track of all of your calls for you. It returns a list of Spy::MethodCall objects which give you even more rich features:

fruit = Fruit.new
spy = Spy.on(fruit, :eat)
fruit.eat(:like_a_boss)
fruit.eat(:on_a_boat)
spy.call_history
#=> [
  #<Spy::MethodCall:0x007fd1db0dc6e0 @replayer=#<Proc:0x007fd1db0dc730@/Users/Bodah/.rbenv/versions/2.1.3/lib/ruby/gems/2.1.0/gems/spy_rb-0.3.0/lib/spy/instance/api/internal.rb:60>, @name=:eat, @receiver=#<Fruit:0x007fd1db0efdd0>, @args=[:like_a_boss], @result=nil>,
  #<Spy::MethodCall:0x007fd1db033c70 @replayer=#<Proc:0x007fd1db033cc0@/Users/Bodah/.rbenv/versions/2.1.3/lib/ruby/gems/2.1.0/gems/spy_rb-0.3.0/lib/spy/instance/api/internal.rb:60>, @name=:eat, @receiver=#<Fruit:0x007fd1db0efdd0>, @args=[:on_a_boat], @result=nil>
]

Spy::MethodCall

Spy::MethodCall has a bunch of useful attributes like #receiver, #args, #caller, #block, #name, and #result. Right now Spy::MethodCall does not deep copy args or results, so be careful!

Spy::MethodCall also has the experimental feature #replay which can be used interactively for debugging:

fruit = Fruit.new
spy = Spy.on(fruit, :eat)
fruit.eat(:quickly)
#=> you take a bite quickly

spy.call_history[0].replay
#=> you take a bite quickly

spy.call_count
#=> 1

Additionally, if you're adventurous you can give Spy::Instance#replay_all a shot:

fruit = Fruit.new
spy = Spy.on(fruit, :eat)
fruit.eat(:quickly)
#=> you take a bite quickly

fruit.eat(:slowly)
#=> you take a bite slowly

spy.call_count
#=> 2

spy.replay_all
#=> you take a bite quickly
#=> you take a bite slowly

spy.call_count
#=> 2

Deploying (note to self)

rake full_deploy TO=0.2.1