Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/ryanong/spy
A simple opinionated mocking framework. All the features of rspec-mocks and more in half the code.
https://github.com/ryanong/spy
Last synced: 2 days ago
JSON representation
A simple opinionated mocking framework. All the features of rspec-mocks and more in half the code.
- Host: GitHub
- URL: https://github.com/ryanong/spy
- Owner: ryanong
- License: mit
- Created: 2012-12-24T19:36:24.000Z (about 12 years ago)
- Default Branch: master
- Last Pushed: 2023-01-03T05:06:21.000Z (about 2 years ago)
- Last Synced: 2025-02-05T19:18:20.129Z (9 days ago)
- Language: Ruby
- Homepage:
- Size: 281 KB
- Stars: 148
- Watchers: 6
- Forks: 23
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.txt
Awesome Lists containing this project
- awesome-rspec - spy - A simple opinionated mocking framework. (Mocks)
README
# Spy
[![Gem Version](https://badge.fury.io/rb/spy.png)](http://badge.fury.io/rb/spy)
[![Build Status](https://travis-ci.org/ryanong/spy.png?branch=master)](https://travis-ci.org/ryanong/spy)
[![Coverage Status](https://coveralls.io/repos/ryanong/spy/badge.png?branch=master)](https://coveralls.io/r/ryanong/spy)[Docs](http://rdoc.info/gems/spy/frames)
Spy is a lightweight stubbing framework with support for method spies, constant stubs, and object mocks.
Spy supports ruby 2.7.0+.
Spy features that were completed were tested against the rspec-mocks tests so it covers all cases that rspec-mocks does.
Inspired by the spy api of the jasmine javascript testing framework.
## Why use this instead of rspec-mocks, mocha, or etc
* Spy will raise error when you try to stub/spy a method that doesn't exist
* when you change your method name your unit tests will break
* no more fantasy tests
* Spy arity matches original method
* Your tests will raise an error if you use the wrong arity
* Spy visibility matches original method
* Your tests will raise an error if you try to call the method incorrectly
* Simple call log api
* easier to read tests
* use ruby to test ruby instead of a dsl
* no expectations
* really who thought that was a good idea?
* absolutely no polution of global object space
* no polution of instance variables for stubbed objectsFail faster, code faster.
## Why not to use this
* mocking null objects is not supported(yet)
* no argument matchers for `Spy::Subroutine#has_been_called_with`
* cannot watch all calls to an object to check order in which they are called
* cannot transfer nested constants when stubbing a constant
* i don't think anybody uses this anyway
* nobody on github does
* #with is not supported
* you can usually just check the call logs.
* if you do need to use this. It is probably a code smell. You either need to abstract your method more or add separate tests.
* you want to use dumb double, Spy has smart mocks, they are better
* you use `mock_model` and `stub_model`## Installation
Add this line to your application's Gemfile:
gem 'spy'
And then execute:
$ bundle
Or install it yourself as:
$ gem install spy
## Usage
### Method Stubs
A method stub overrides a pre-existing method and records all calls to specified method. You can set the spy to return either the original method or your own custom implementation.
Spy support 2 different ways of spying an existing method on an object.
```ruby
Spy.on(book, title: "East of Eden")
Spy.on(book, :title).and_return("East of Eden")
Spy.on(book, :title).and_return { "East of Eden" }book.title #=> "East of Eden"
```Spy will raise an error if you try to stub on a method that doesn't exist.
You can force the creation of a stub on method that didn't exist but it really isn't suggested.```ruby
Spy::Subroutine.new(book, :flamethrower).hook(force:true).and_return("burnninante")
```You can also stub instance methods of Classes and Modules. This is equivalent to
rspec-mock's `allow_any_instance_of(Module)````ruby
Spy.on_instance_method(Book, :title).and_return("Cannery Row")Book.new(title: "Siddhartha").title #=> "Cannery Row"
Book.new(title: "The Big Cheese").title #=> "Cannery Row"
```### Test Mocks
A test mock is an object that quacks like a given class but will raise an error
when the method is not stubbed. Spy will not let you stub a method that wasn't
on the mocked class. You can spy on the classes and call through to the original method.```ruby
book = Spy.mock(Book) # Must be a class
Spy.on(book, first_name: "Neil", last_name: "Gaiman")
Spy.on(book, :author).and_call_through
book.author #=> "Neil Gaiman"book.responds_to? :title #=> true
book.title #=> Spy::NeverHookedError: 'title' was never hooked on mock spy.
```To stub methods during instantiation just add arguments.
```ruby
book = Spy.mock(Book, :first_name, author: "Neil Gaiman")
```### Arbitrary Handling
If you need to have a custom method based in the method inputs just send a block to `#and_return`
```ruby
Spy.on(book, :read_page).and_return do |page, &block|
block.call
"awesome " * page
end
```An error will raise if the arity of the block is larger than the arity of the original method. However this can be overidden with the force argument.
```ruby
Spy.on(book, :read_page).and_return(force: true) do |a, b, c, d|
end
```### Method Spies
When you stub a method it returns a spy. A spy records what calls have been made to a given method.
```ruby
validator = Spy.mock(Validator)
validate_spy = Spy.on(validator, :validate)
validate_spy.has_been_called? #=> falsevalidator.validate("01234") #=> nil
validate_spy.has_been_called? #=> true
validate_spy.has_been_called_with?("01234") #=> true
```You can also retrieve a method spy on demand
```ruby
Spy.get(validator, :validate)
```### Calling through
If you just want to make sure if a method is called and not override the output you can just use the `#and_call_through` method```ruby
Spy.on(book, :read_page).and_call_through
```By if the original method never existed it will call `#method_missing` on the spied object.
### Call Logs
When a spy is called on it records a call log. A call log contains the object it was called on, the arguments and block that were sent to method and what it returned.
```ruby
read_page_spy = Spy.on(book, read_page: "hello world")
book.read_page(5) { "this is a block" }
book.read_page(3)
book.read_page(7)
read_page_spy.calls.size #=> 3
first_call = read_page_spy.calls.first
first_call.object #=> book
first_call.args #=> [5]
first_call.block #=> Proc.new { "this is a block" }
first_call.result #=> "hello world"
first_call.called_from #=> "file_name.rb:line_number"
```## Test Framework Integration
### MiniTest/TestUnit
in your `test_helper.rb` add this line after you include your framework
```ruby
require 'spy/integration'
```In your test file
```ruby
def test_title
book = Book.new
title_spy = Spy.on(book, :title)
book.title
book.titleassert_received book, :title
assert title_spy.has_been_called?
assert_equal 2, title_spy.calls.count
end
```### Rspec
In `spec_helper.rb`
```ruby
require "rspec/autorun"
require "spy/integration"
RSpec.configure do |c|
c.mock_with Spy::RspecAdapter
end
```In your test
```ruby
describe Book do
it "title can be called" do
book = book.new
page_spy = Spy.on(book, page)
book.page(1)
book.page(2)expect(book).to have_received(:page)
expect(book).to have_received(:page).with(1)
expect(book).to have_received(:page).with(2)expect(page_spy).to have_been_called
expect(page_spy.calls.count).to eq(2)
end
end
```## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request