Skip to content

jbodah/spy_rb

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

About

🔍 Transparent Test Spies for Ruby

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages