├── VERSION ├── lib ├── gimme-double.rb ├── gimme │ ├── errors.rb │ ├── verifies_class_methods.rb │ ├── rspec_adapter.rb │ ├── invocation_store.rb │ ├── stubbing_store.rb │ ├── captor.rb │ ├── invokes_satisfied_stubbing.rb │ ├── gives.rb │ ├── reset.rb │ ├── method_store.rb │ ├── ensures_class_method_restoration.rb │ ├── finds_stubbings.rb │ ├── compares_args.rb │ ├── store.rb │ ├── spies_on_class_methods.rb │ ├── gives_class_methods.rb │ ├── matchers.rb │ ├── resolves_methods.rb │ ├── verifies.rb │ ├── dsl.rb │ └── test_double.rb └── gimme.rb ├── features ├── support │ ├── hooks.rb │ ├── env.rb │ └── animals.rb ├── old │ ├── stub_sensible_defaults.feature │ ├── argument_captors.feature │ ├── gimme_next.feature │ ├── verify_no_args.feature │ ├── stub_class_methods.feature │ ├── verify_with_args.feature │ ├── verify_matcher_anything.feature │ ├── unknown_methods.feature │ ├── stub_basic.feature │ └── stub_matchers.feature ├── readme.md ├── class_methods.feature ├── messages.feature ├── step_definitions │ ├── doc_steps.rb │ └── gimme_steps.rb ├── basics.feature └── matchers.feature ├── .document ├── .gitignore ├── spec ├── gimme │ ├── errors_spec.rb │ ├── invocation_store_spec.rb │ ├── rspec_adapter_spec.rb │ ├── gives_spec.rb │ ├── verifies_spec.rb │ ├── captor_spec.rb │ ├── shared_examples │ │ ├── shared_gives_examples.rb │ │ └── shared_verifies_examples.rb │ ├── verifies_class_methods_spec.rb │ ├── gives_class_methods_spec.rb │ ├── test_double_spec.rb │ ├── spies_on_class_method_spec.rb │ ├── resolves_methods_spec.rb │ └── matchers_spec.rb └── spec_helper.rb ├── .travis.yml ├── Guardfile ├── Gemfile ├── README.rdoc ├── LICENSE.txt ├── Rakefile ├── Gemfile.lock ├── gimme.gemspec └── README.markdown /VERSION: -------------------------------------------------------------------------------- 1 | 0.5.0 -------------------------------------------------------------------------------- /lib/gimme-double.rb: -------------------------------------------------------------------------------- 1 | require 'gimme' -------------------------------------------------------------------------------- /features/support/hooks.rb: -------------------------------------------------------------------------------- 1 | After do 2 | Gimme.reset 3 | end -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /lib/gimme/errors.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | module Errors 3 | class VerificationFailedError < StandardError 4 | end 5 | end 6 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | coverage 3 | rdoc 4 | doc 5 | .yardoc 6 | .bundle 7 | pkg 8 | lib/driver.rb 9 | .rbx 10 | .ruby-version 11 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.expand_path('../../../lib',__FILE__) 2 | $LOAD_PATH << File.expand_path('../',__FILE__) 3 | require 'gimme' 4 | require 'animals' -------------------------------------------------------------------------------- /spec/gimme/errors_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Gimme 4 | describe Errors do 5 | specify { Errors::VerificationFailedError.kind_of? StandardError } 6 | end 7 | end -------------------------------------------------------------------------------- /lib/gimme/verifies_class_methods.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | 3 | class VerifiesClassMethods < Verifies 4 | def __gimme__cls 5 | (class << @double; self; end) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | install: bundle install --path vendor/bundle 3 | rvm: 4 | - 1.8.7 5 | - 1.9.2 6 | - 1.9.3 7 | - 2.0.0 8 | - jruby-18mode # JRuby in 1.8 mode 9 | - jruby-19mode # JRuby in 1.9 mode 10 | - ree 11 | -------------------------------------------------------------------------------- /lib/gimme/rspec_adapter.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | module RSpecAdapter 3 | def setup_mocks_for_rspec 4 | end 5 | 6 | def verify_mocks_for_rspec 7 | end 8 | 9 | def teardown_mocks_for_rspec 10 | Gimme.reset 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start do 3 | add_filter "/spec/" 4 | end 5 | 6 | require 'coveralls' 7 | Coveralls.wear! 8 | 9 | require 'gimme' 10 | require 'rspec/given' 11 | 12 | RSpec.configure do |config| 13 | config.mock_framework = Gimme::RSpecAdapter 14 | end 15 | -------------------------------------------------------------------------------- /lib/gimme/invocation_store.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | 3 | class InvocationStore < Store 4 | 5 | def increment(double, method, args) 6 | set(double, method, args, get(double, method, args).to_i.succ) 7 | end 8 | end 9 | 10 | def self.invocations 11 | @@invocations ||= InvocationStore.new 12 | end 13 | 14 | end -------------------------------------------------------------------------------- /lib/gimme/stubbing_store.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | 3 | class StubbingStore < Store 4 | 5 | def invoke(double, method, args) 6 | if stubbing = get(double, method, args) 7 | stubbing.call 8 | end 9 | end 10 | 11 | end 12 | 13 | def self.stubbings 14 | @@stubbings ||= StubbingStore.new 15 | end 16 | 17 | end -------------------------------------------------------------------------------- /spec/gimme/invocation_store_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Gimme 4 | describe InvocationStore do 5 | describe "incrementing calls" do 6 | When { 3.times { subject.increment(:some_double, :some_method, :some_args) } } 7 | Then { subject.get(:some_double, :some_method, :some_args).should == 3 } 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /features/old/stub_sensible_defaults.feature: -------------------------------------------------------------------------------- 1 | Feature: Default returns 2 | 3 | As a test author 4 | I want my test double to have some sensible defaults 5 | so that I do not find myself writing redundant/obvious stub code 6 | 7 | Scenario: query? methods' default stubbing is false 8 | Given a new Dog test double 9 | Then invoking purebred? returns false 10 | And invoking walk_to(1,1) returns nil 11 | -------------------------------------------------------------------------------- /spec/gimme/rspec_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Gimme::RSpecAdapter 4 | describe "subject" do 5 | Then { setup_mocks_for_rspec } 6 | Then { verify_mocks_for_rspec } 7 | 8 | describe "#teardown_mocks_for_rspec" do 9 | # Given { spy_on(Gimme).reset } 10 | When { RSpecAdapter.teardown_mocks_for_rspec } 11 | # Then { verify(Gimme).reset } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/gimme/captor.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | 3 | class Captor 4 | attr_accessor :value 5 | end 6 | 7 | module Matchers 8 | class Capture < Matchers::Matcher 9 | def initialize(captor) 10 | @captor = captor 11 | end 12 | 13 | def matches?(arg) 14 | @captor.value = arg 15 | true 16 | end 17 | end 18 | 19 | def capture(captor) 20 | Capture.new(captor) 21 | end 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /lib/gimme/invokes_satisfied_stubbing.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | class InvokesSatisfiedStubbing 3 | def initialize(stubbed_thing) 4 | @finder = FindsStubbings.new(stubbed_thing) 5 | end 6 | 7 | def invoke(method, args, block = nil) 8 | if blk = @finder.find(method, args) 9 | blk.call(block) 10 | elsif method.to_s[-1,1] == '?' 11 | false 12 | else 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/gimme/gives.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | 3 | class Gives < BlankSlate 4 | attr_accessor :raises_no_method_error 5 | def initialize(double) 6 | @double = double 7 | @raises_no_method_error = true 8 | end 9 | 10 | def method_missing(sym, *args, &block) 11 | sym = ResolvesMethods.new(@double.cls,sym,args).resolve(@raises_no_method_error) 12 | 13 | Gimme.stubbings.set(@double, sym, args, block) 14 | end 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /lib/gimme/reset.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | @@stuff_to_do_on_reset = [] 3 | @@stuff_to_do_on_every_reset = [] 4 | 5 | def self.on_reset (situation = :once, &blk) 6 | if situation == :once 7 | @@stuff_to_do_on_reset << blk 8 | else 9 | @@stuff_to_do_on_every_reset << blk 10 | end 11 | end 12 | 13 | def self.reset 14 | @@stuff_to_do_on_reset.delete_if { |stuff| stuff.call || true } 15 | @@stuff_to_do_on_every_reset.each { |stuff| stuff.call } 16 | end 17 | end -------------------------------------------------------------------------------- /lib/gimme/method_store.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | 3 | class ClassMethodStore < Store 4 | 5 | def set(cls, method_name) 6 | id = cls.__id__ 7 | meta = (class << cls; self; end) 8 | @store[id] ||= {} 9 | 10 | if meta.method_defined?(method_name) && !@store[id][method_name] 11 | @store[id][method_name] = cls.method(method_name) 12 | end 13 | end 14 | 15 | end 16 | 17 | def self.class_methods 18 | @@class_methods ||= ClassMethodStore.new 19 | end 20 | 21 | end -------------------------------------------------------------------------------- /features/old/argument_captors.feature: -------------------------------------------------------------------------------- 1 | Feature: Capturing Arguments 2 | 3 | As a test author 4 | I want to capture the value of an argument passed to my test double 5 | So that I can assert things about arguments my system under test passes to its collaborators 6 | 7 | Scenario: capturing an argument 8 | Given a new Dog test double 9 | And a new argument captor 10 | When I invoke holler_at(:panda) 11 | Then I can verify holler_at(capture(@captor)) has been invoked 1 time 12 | And the captor's value is :panda -------------------------------------------------------------------------------- /lib/gimme/ensures_class_method_restoration.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | class EnsuresClassMethodRestoration 3 | def initialize(cls) 4 | @cls = cls 5 | end 6 | 7 | def ensure(method) 8 | meta_class = (class << @cls; self; end) 9 | Gimme.on_reset do 10 | if real_method = Gimme.class_methods.get(@cls, method) 11 | meta_class.instance_eval { define_method method, real_method } 12 | else 13 | meta_class.send(:remove_method, method) 14 | end 15 | end 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'rspec', :cli => '--color', :version => 2 do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 7 | watch('spec/spec_helper.rb') { "spec" } 8 | end 9 | 10 | guard 'cucumber' do 11 | watch(%r{^features/.+\.feature$}) 12 | watch(%r{^features/support/.+$}) { 'features' } 13 | watch(%r{^features/step_definitions/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'features' } 14 | end 15 | -------------------------------------------------------------------------------- /features/old/gimme_next.feature: -------------------------------------------------------------------------------- 1 | Feature: Gimme Next 2 | 3 | As a test author 4 | I want a test double to stand-in for the next new'ing of a given class 5 | so that I can isolate my SUT and stub & verify behavior of collaborators instantiated by the SUT 6 | 7 | Scenario: 8 | Given I create a double via gimme_next(Turtle) 9 | When my SUT tries creating a real Turtle.new(:shell) 10 | And I invoke swim 11 | Then both the double and real object reference the same object 12 | And I can verify! initialize(:shell) has been invoked 1 time 13 | And I can verify swim has been invoked 1 time -------------------------------------------------------------------------------- /lib/gimme/finds_stubbings.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | class FindsStubbings 3 | def initialize(stubbed_thing) 4 | @stubbings = Gimme.stubbings.get(stubbed_thing) || {} 5 | end 6 | 7 | def count(method, args) 8 | stubbings_for(method, args).size 9 | end 10 | 11 | def find(method, args) 12 | stubbings_for(method, args).last 13 | end 14 | 15 | private 16 | def stubbings_for(method, args) 17 | return [] unless @stubbings[method] 18 | @stubbings[method].find { |(stub_args, blk)| ComparesArgs.new(args, stub_args).match? } || [] 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /features/readme.md: -------------------------------------------------------------------------------- 1 | # gimme 2 | 3 | Gimme is a test double library designed to help you write isolated unit specs for your Ruby code. 4 | 5 | Here are a few examples: 6 | 7 | * [Basic usage](https://www.relishapp.com/searls/gimme/docs/basic-usage) 8 | * [Argument matchers](https://www.relishapp.com/searls/gimme/docs/argument-matchers) (and argument captors) 9 | * [Stubbing and verifying class methods](https://www.relishapp.com/searls/gimme/docs/class-methods) 10 | 11 | Todo: 12 | 13 | * Overriding method existence checks with ! (e.g. `give!()` or `verify!()`) 14 | * Stubbing the next `new`'ing of a type (e.g. `gimme_next(Dog)` ) -------------------------------------------------------------------------------- /lib/gimme/compares_args.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | class ComparesArgs 3 | 4 | def initialize(actual, expected) 5 | @actual = actual 6 | @expected = expected 7 | end 8 | 9 | def match? 10 | same_size? && same_args? 11 | end 12 | 13 | private 14 | 15 | def same_size? 16 | @actual.size == @expected.size 17 | end 18 | 19 | def same_args? 20 | @actual.each_index.all? do |i| 21 | @actual[i] == @expected[i] || matchers?(@expected[i], @actual[i]) 22 | end 23 | end 24 | 25 | def matchers?(matcher, arg) 26 | matcher.respond_to?(:matches?) && matcher.matches?(arg) 27 | end 28 | 29 | end 30 | end -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | # Add dependencies required to use your gem here. 3 | 4 | # Add dependencies to develop your gem here. 5 | # Include everything needed to run rake, tests, features, etc. 6 | group :development, :test do 7 | gem 'pry' 8 | 9 | gem 'rdoc' 10 | gem 'rake' 11 | 12 | gem "rspec" 13 | gem "rspec-given" 14 | gem "cucumber" 15 | gem "simplecov", :require => false 16 | gem 'coveralls', :require => false 17 | gem 'jeweler' 18 | 19 | gem "guard-cucumber", :require => false 20 | gem "guard-rspec", :require => false 21 | if RUBY_PLATFORM =~ /darwin/i 22 | gem "growl" 23 | gem "rb-fsevent", :require => false 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/gimme.rb: -------------------------------------------------------------------------------- 1 | require 'gimme/reset' 2 | require 'gimme/test_double' 3 | require 'gimme/resolves_methods' 4 | require 'gimme/errors' 5 | require 'gimme/gives' 6 | require 'gimme/gives_class_methods' 7 | require 'gimme/ensures_class_method_restoration' 8 | require 'gimme/verifies' 9 | require 'gimme/spies_on_class_methods' 10 | require 'gimme/verifies_class_methods' 11 | require 'gimme/matchers' 12 | require 'gimme/captor' 13 | require 'gimme/invokes_satisfied_stubbing' 14 | require 'gimme/dsl' 15 | require 'gimme/store' 16 | require 'gimme/stubbing_store' 17 | require 'gimme/finds_stubbings' 18 | require 'gimme/invocation_store' 19 | require 'gimme/method_store' 20 | require 'gimme/compares_args' 21 | 22 | require 'gimme/rspec_adapter' 23 | 24 | include Gimme::Matchers 25 | include Gimme::DSL -------------------------------------------------------------------------------- /features/old/verify_no_args.feature: -------------------------------------------------------------------------------- 1 | Feature: verification of no-arg methods 2 | 3 | As a test author 4 | I want to verify my test double's no-arg method was invoked 5 | So that I can specify its behavior 6 | 7 | Scenario: 8 | Given a new test double 9 | But I do not invoke yawn 10 | Then verifying yawn raises a Gimme::Errors::VerificationFailedError 11 | But I can verify yawn has been invoked 0 times 12 | 13 | When I invoke yawn 14 | Then I can verify yawn has been invoked 15 | And I can verify yawn has been invoked 1 time 16 | 17 | When I invoke yawn 18 | Then I can verify yawn has been invoked 2 times 19 | 20 | Scenario: class methods 21 | Given the ClassyPossum class 22 | When I spy on ClassyPossum.yawn 23 | Then it can verify no-arg methods too. -------------------------------------------------------------------------------- /lib/gimme/store.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | class Store 3 | @store = {} 4 | 5 | def initialize 6 | @store = {} 7 | Gimme.on_reset(:each) do 8 | @store.clear 9 | end 10 | end 11 | 12 | def reset 13 | @store = {} 14 | end 15 | 16 | def set(double, method, args, content) 17 | id = double.__id__ 18 | @store[id] ||= {} 19 | @store[id][method] ||= {} 20 | @store[id][method][args] = content 21 | end 22 | 23 | def get(double, method=nil, args=nil) 24 | id = double.__id__ 25 | if !method 26 | @store[id] 27 | elsif @store[id] && !args 28 | @store[id][method] 29 | elsif @store[id] && @store[id][method] 30 | @store[id][method][args] 31 | end 32 | end 33 | 34 | def clear 35 | @store.clear 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /lib/gimme/spies_on_class_methods.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | class SpiesOnClassMethod 3 | attr_accessor :raises_no_method_error 4 | def initialize(cls) 5 | @cls = cls 6 | @raises_no_method_error = true 7 | end 8 | 9 | def spy(method) 10 | meta_class = (class << @cls; self; end) 11 | method = ResolvesMethods.new(meta_class, method).resolve(@raises_no_method_error) 12 | 13 | if meta_class.method_defined? method 14 | Gimme.class_methods.set(@cls, method) 15 | meta_class.send(:undef_method, method) 16 | end 17 | 18 | cls = @cls 19 | meta_class.instance_eval do 20 | define_method method do |*args| 21 | Gimme.invocations.increment(cls, method, args) 22 | end 23 | end 24 | 25 | EnsuresClassMethodRestoration.new(@cls).ensure(method) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /features/old/stub_class_methods.feature: -------------------------------------------------------------------------------- 1 | Feature: stubbing class methods 2 | 3 | In order to spec code that depends on Rails or other APIs that make 4 | liberal use of class methods, I want to stub class methods 5 | and see that the classes are restored after each spec run. 6 | 7 | 8 | Scenario Outline: stubbing class method 9 | Given the Possum class 10 | When I stub to return 11 | Then invoking returns 12 | 13 | When I reset Gimme 14 | Then no longer returns 15 | 16 | Scenarios: the anything matcher with a one-argument method 17 | | method | gives | invocation | returns | 18 | | crawl_to(anything) | '…' | crawl_to(Cat.new) | '…' | 19 | | crawl_to(anything) | '…' | crawl_to(Dog.new) | '…' | 20 | | crawl_to(anything) | '…' | crawl_to(nil) | '…' | 21 | -------------------------------------------------------------------------------- /features/old/verify_with_args.feature: -------------------------------------------------------------------------------- 1 | Feature: verification of argumentative methods 2 | 3 | As a test author 4 | I want to verify my test double's argument-having method was invoked 5 | So that I can specify the exact arguments 6 | 7 | Scenario: 8 | Given a new test double 9 | But I do not invoke equal?(:pants) 10 | Then verifying equal?(:pants) raises a Gimme::Errors::VerificationFailedError 11 | But I can verify equal?(:pants) has been invoked 0 times 12 | 13 | When I invoke equal?(:pants) 14 | Then I can verify equal?(:pants) has been invoked 15 | And I can verify equal?(:pants) has been invoked 1 time 16 | And I can verify equal?(:kaka) has been invoked 0 times 17 | 18 | When I invoke equal?(:pants) 19 | And I invoke equal?(:kaka) 20 | Then I can verify equal?(:kaka) has been invoked 1 time 21 | And I can verify equal?(:pants) has been invoked 2 times -------------------------------------------------------------------------------- /spec/gimme/gives_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gimme/shared_examples/shared_gives_examples' 3 | 4 | module Gimme 5 | describe Gives do 6 | 7 | class Bunny 8 | def nibble 9 | end 10 | 11 | def eat(food) 12 | end 13 | end 14 | 15 | context "using the class API" do 16 | Given(:subject) { TestDouble.new(Bunny) } 17 | 18 | it_behaves_like "a normal stubbing" do 19 | Given(:gives) { Gives.new(subject) } 20 | end 21 | 22 | it_behaves_like "an overridden stubbing" do 23 | Given(:gives) { Gives.new(subject) } 24 | end 25 | end 26 | 27 | context "using the gimme DSL" do 28 | Given(:subject) { gimme(Bunny) } 29 | 30 | it_behaves_like "a normal stubbing" do 31 | Given(:gives) { give(subject) } 32 | end 33 | 34 | it_behaves_like "an overridden stubbing" do 35 | Given(:gives) { give!(subject) } 36 | end 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /spec/gimme/verifies_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gimme/shared_examples/shared_verifies_examples' 3 | 4 | module Gimme 5 | 6 | describe Verifies do 7 | class Natto 8 | def ferment(beans=nil,time=nil) 9 | end 10 | end 11 | 12 | Given(:test_double) { gimme(Natto) } 13 | Given(:double_name) { "Gimme::Natto" } 14 | Given(:verifier_class) { Verifies } 15 | 16 | context "class API" do 17 | Given(:verifier) { Verifies.new(test_double) } 18 | it_behaves_like "a verifier" 19 | it_behaves_like "an overridden verifier" do 20 | Given { verifier.raises_no_method_error = false } 21 | end 22 | end 23 | 24 | context "gimme DSL" do 25 | it_behaves_like "a verifier" do 26 | Given(:verifier) { verify(test_double) } 27 | end 28 | 29 | it_behaves_like "an overridden verifier" do 30 | Given(:verifier) { verify!(test_double) } 31 | end 32 | end 33 | end 34 | end -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = gimme 2 | 3 | gimme is a lightweight test double library for ruby 4 | 5 | Project site: http://github.com/searls/gimme 6 | Gimme features: http://relishapp.com/searls/gimme 7 | 8 | == Contributing to gimme 9 | 10 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet 11 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it 12 | * Fork the project 13 | * Start a feature/bugfix branch 14 | * Commit and push until you are happy with your contribution 15 | * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. 16 | * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. 17 | 18 | == Copyright 19 | 20 | Copyright (c) 2010 Justin Searls. See LICENSE.txt for 21 | further details. 22 | 23 | -------------------------------------------------------------------------------- /spec/gimme/captor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Gimme 4 | describe Captor do 5 | describe "#value" do 6 | Given(:captor) { Captor.new } 7 | When { captor.value = "panda" } 8 | Then { captor.value.should == "panda" } 9 | end 10 | end 11 | 12 | describe Matchers::Capture do 13 | Given(:captor) { Captor.new } 14 | 15 | shared_examples_for "an argument captor" do 16 | describe "#matches?" do 17 | When(:result) { a_capture.matches?("anything at all") } 18 | Then { result.should == true } 19 | Then { captor.value.should == "anything at all" } 20 | end 21 | end 22 | 23 | context "using the class API" do 24 | it_behaves_like "an argument captor" do 25 | Given(:a_capture) { Capture.new(captor) } 26 | end 27 | end 28 | 29 | context "using gimme DSL" do 30 | it_behaves_like "an argument captor" do 31 | Given(:a_capture) { capture(captor) } 32 | end 33 | end 34 | end 35 | end -------------------------------------------------------------------------------- /features/class_methods.feature: -------------------------------------------------------------------------------- 1 | Feature: class methods 2 | 3 | Scenario: stubbing and verifying behavior 4 | Given we have this production code: 5 | """ 6 | class Cat 7 | def interact(type) 8 | end 9 | end 10 | 11 | class CatRepository 12 | def self.find(cat_id) 13 | end 14 | end 15 | """ 16 | When we want to write some tests to help us write this method: 17 | """ 18 | class ScratchesCat 19 | def scratch(cat_id) 20 | CatRepository.find(cat_id).interact(:scratch) 21 | end 22 | end 23 | """ 24 | Then we can use gimme to isolate the unit under test: 25 | """ 26 | cat = gimme(Cat) 27 | give(CatRepository).find(12) { cat } 28 | 29 | ScratchesCat.new.scratch(12) 30 | 31 | verify(cat).interact(:scratch) 32 | 33 | # to clear any class method stubbings, do this: 34 | Gimme.reset # in RSpec, Gimme::RSpecAdapter can do this on teardown for you. 35 | """ 36 | -------------------------------------------------------------------------------- /lib/gimme/gives_class_methods.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | 3 | class GivesClassMethods < BlankSlate 4 | attr_accessor :raises_no_method_error 5 | def initialize(cls) 6 | @cls = cls 7 | @raises_no_method_error = true 8 | end 9 | 10 | def method_missing(method, *args, &block) 11 | cls = @cls 12 | meta_class = (class << cls; self; end) 13 | method = ResolvesMethods.new(meta_class,method,args).resolve(@raises_no_method_error) 14 | 15 | Gimme.class_methods.set(cls, method) 16 | Gimme.stubbings.set(cls, method, args, block) 17 | 18 | #TODO this will be redundantly overwritten 19 | meta_class.instance_eval do 20 | define_method method do |*actual_args, &actual_block| 21 | Gimme.invocations.increment(cls, method, actual_args) 22 | InvokesSatisfiedStubbing.new(cls).invoke(method, actual_args, actual_block) 23 | end 24 | end 25 | 26 | EnsuresClassMethodRestoration.new(@cls).ensure(method) 27 | end 28 | 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /features/old/verify_matcher_anything.feature: -------------------------------------------------------------------------------- 1 | Feature: verify with an anything matcher 2 | 3 | As a test author 4 | I want to be able to verify an invocation replacing exact arguments with the anything matcher 5 | so that I'm able to specify only the parameters that matter to me 6 | 7 | Scenario: a single-argument 8 | Given a new Dog test double 9 | Then I can verify holler_at(anything) has been invoked 0 times 10 | When I invoke holler_at(false) 11 | Then I can verify holler_at(anything) has been invoked 1 time 12 | When I invoke holler_at(true) 13 | Then I can verify holler_at(anything) has been invoked 2 times 14 | 15 | Scenario: two arguments 16 | Given a new Dog test double 17 | Then I can verify walk_to(anything,anything) has been invoked 0 times 18 | When I invoke walk_to(12.34,943.1) 19 | Then I can verify walk_to(anything,anything) has been invoked 1 time 20 | And I can verify walk_to(anything,943.1) has been invoked 1 time 21 | And I can verify walk_to(12.34,anything) has been invoked 1 time -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Justin Searls 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/gimme/matchers.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | module Matchers 3 | class Matcher 4 | def matches?(arg) 5 | false 6 | end 7 | end 8 | 9 | class Anything < Matcher 10 | def matches?(arg) 11 | true 12 | end 13 | end 14 | def anything 15 | Gimme::Matchers::Anything.new 16 | end 17 | 18 | class IsA < Matcher 19 | def initialize(cls) 20 | @cls = cls 21 | end 22 | 23 | def matches?(arg) 24 | arg.kind_of?(@cls) 25 | end 26 | end 27 | def is_a(cls) 28 | Gimme::Matchers::IsA.new(cls) 29 | end 30 | 31 | class Any < IsA 32 | def matches?(arg) 33 | arg == nil || arg.kind_of?(@cls) 34 | end 35 | end 36 | def any(cls) 37 | Gimme::Matchers::Any.new(cls) 38 | end 39 | 40 | class Numeric < Matcher 41 | def matches?(arg) 42 | arg.kind_of?(Fixnum) || arg.kind_of?(Numeric) || arg.kind_of?(Float) 43 | end 44 | end 45 | def numeric 46 | Gimme::Matchers::Numeric.new 47 | end 48 | 49 | class Boolean < Matcher 50 | def matches?(arg) 51 | arg.kind_of?(TrueClass) || arg.kind_of?(FalseClass) 52 | end 53 | end 54 | def boolean 55 | Gimme::Matchers::Boolean.new 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/gimme/resolves_methods.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | 3 | class ResolvesMethods 4 | def initialize(cls, sym, args=[]) 5 | @cls = cls 6 | @sym = sym 7 | @args = args 8 | end 9 | 10 | def resolve(raises_no_method_error=true) 11 | @sym = @args.shift if @sym == :send 12 | if @cls && raises_no_method_error 13 | if @cls.private_method_defined?(named(@sym)) 14 | raise NoMethodError.new("#{@sym} is a private method of your #{@cls} test double, so stubbing/verifying it 15 | might not be a great idea. If you want to try to stub or verify this method anyway, then you can 16 | invoke give! or verify! to suppress this error.") 17 | elsif !@cls.instance_methods.include?(named(@sym)) 18 | raise NoMethodError.new("Your test double of #{@cls} may not know how to respond to the '#{@sym}' method. 19 | If you're confident that a real #{@cls} will know how to respond to '#{@sym}', then you can 20 | invoke give! or verify! to suppress this error.") 21 | end 22 | end 23 | 24 | @sym 25 | end 26 | 27 | private 28 | 29 | if RUBY_VERSION >= "1.9.2" 30 | def named(sym) 31 | sym 32 | end 33 | else 34 | def named(sym) 35 | sym.to_s 36 | end 37 | end 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /features/messages.feature: -------------------------------------------------------------------------------- 1 | Feature: messages from test doubles 2 | 3 | Test doubles need to output sufficient messages 4 | (particularly on failure) 5 | 6 | Scenario: inspect/to_s for arguments 7 | Given we have this production code: 8 | """ 9 | class Chair 10 | end 11 | 12 | class Person 13 | def 14 | sit_on(thing) 15 | end 16 | end 17 | """ 18 | When we write a test we expect to fail: 19 | """ 20 | chair = gimme(Chair) 21 | person = gimme(Person) 22 | 23 | person.sit_on() #<--oops! forgot the chair 24 | 25 | verify(person).sit_on(chair) 26 | """ 27 | Then we should see a failure message that includes: 28 | """ 29 | expected Person#sit_on to have been called with arguments \[?<#Gimme:1 Chair>\]? 30 | """ 31 | Then we should see a failure message that includes: 32 | """ 33 | was actually called 1 times with arguments \[?\]? 34 | """ 35 | 36 | Scenario: naming mocks 37 | Given we have this production code: 38 | """ 39 | class Panda 40 | end 41 | """ 42 | Then this should work: 43 | """ 44 | gimme(Panda).to_s.should == "<#Gimme:1 Panda>" 45 | gimme(Panda).inspect.should == "<#Gimme:2 Panda>" 46 | gimme("a bear thing").inspect.should == "<#Gimme:3 a bear thing>" 47 | """ -------------------------------------------------------------------------------- /spec/gimme/shared_examples/shared_gives_examples.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | shared_examples_for "a normal stubbing" do 3 | describe "stubbing an existing method" do 4 | context "no args" do 5 | When { gives.nibble() { "nom" } } 6 | Then { subject.nibble.should == "nom" } 7 | end 8 | 9 | context "with args" do 10 | When { gives.eat("carrot") { "crunch" } } 11 | Then { subject.eat("carrot").should == "crunch" } 12 | Then { subject.eat("apple").should == nil } 13 | # Then { lambda{ subject.eat }.should raise_error ArgumentError } # " } 14 | Given(:verifier_class) { VerifiesClassMethods } 15 | 16 | context "class API" do 17 | Given { SpiesOnClassMethod.new(Natto).spy(:ferment) } 18 | 19 | Given(:verifier) { VerifiesClassMethods.new(test_double) } 20 | 21 | it_behaves_like "a verifier" 22 | 23 | it_behaves_like "an overridden verifier" do 24 | Given do 25 | s = SpiesOnClassMethod.new(Natto) 26 | s.raises_no_method_error = false 27 | s.spy(:eat) 28 | end 29 | Given { verifier.raises_no_method_error = false } 30 | end 31 | end 32 | 33 | context "gimme DSL" do 34 | Given { spy_on(Natto, :ferment)} 35 | 36 | it_behaves_like "a verifier" do 37 | Given(:verifier) { verify(test_double) } 38 | end 39 | 40 | it_behaves_like "an overridden verifier" do 41 | Given { spy_on!(Natto, :eat) } 42 | Given(:verifier) { verify!(test_double) } 43 | end 44 | end 45 | 46 | context "without a spy" do 47 | Given { give!(test_double).season } 48 | When { test_double.season } 49 | Then { verify(test_double).season } 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/gimme/verifies.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | 3 | class Verifies < BlankSlate 4 | attr_accessor :raises_no_method_error 5 | def initialize(double,times=1) 6 | @double = double 7 | @times = times.respond_to?(:count) ? times.count : times 8 | @raises_no_method_error = true 9 | end 10 | 11 | def __gimme__cls 12 | @double.cls 13 | end 14 | 15 | def method_missing(sym, *args, &block) 16 | sym = ResolvesMethods.new(__gimme__cls,sym,args).resolve(@raises_no_method_error) 17 | 18 | if @times != invocation_count(sym, args) 19 | raise Errors::VerificationFailedError.new(message_for(sym, args)) 20 | end 21 | end 22 | 23 | private 24 | 25 | def invocation_count(sym, args) 26 | invocations = Gimme.invocations.get(@double, sym) 27 | return 0 unless invocations 28 | invocations.inject(0) do |memo, (invoke_args, count)| 29 | ComparesArgs.new(invoke_args, args).match? ? memo + count : memo 30 | end 31 | end 32 | 33 | def message_for(sym, args) 34 | msg = "expected #{__gimme__cls || @double}##{sym} to have been called with arguments #{args}" 35 | if !Gimme.invocations.get(@double, sym) || Gimme.invocations.get(@double, sym).empty? 36 | msg << "\n but was never called" 37 | else 38 | msg = Gimme.invocations.get(@double, sym).inject msg do |memo, actual| 39 | memo + "\n was actually called #{actual[1]} times with arguments #{actual[0]}" 40 | end 41 | end 42 | end 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /lib/gimme/dsl.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | module DSL 3 | 4 | # Instantiation 5 | 6 | def gimme(cls=nil) 7 | Gimme::TestDouble.new(cls) 8 | end 9 | 10 | def gimme_next(cls) 11 | double = Gimme::TestDouble.new(cls) 12 | meta_class = class << cls; self; end 13 | real_new = cls.method(:new) 14 | meta_class.send(:define_method,:new) do |*args| 15 | double.send(:initialize,*args) 16 | meta_class.send(:define_method,:new,real_new) #restore :new on the class 17 | double 18 | end 19 | double 20 | end 21 | 22 | 23 | # Stubbing 24 | def give(double) 25 | if double.kind_of? Class 26 | Gimme::GivesClassMethods.new(double) 27 | else 28 | Gimme::Gives.new(double) 29 | end 30 | end 31 | 32 | def give!(double) 33 | give = give(double) 34 | give.raises_no_method_error = false 35 | give 36 | end 37 | 38 | # Verification 39 | def verify(double,times=1) 40 | if double.kind_of? Class 41 | Gimme::VerifiesClassMethods.new(double, times) 42 | else 43 | Gimme::Verifies.new(double,times) 44 | end 45 | end 46 | 47 | def verify!(double,times=1) 48 | verify = verify(double,times) 49 | verify.raises_no_method_error = false 50 | verify 51 | end 52 | 53 | # Spying on class methods 54 | def spy_on(cls, method) 55 | SpiesOnClassMethod.new(cls).spy(method) 56 | end 57 | 58 | def spy_on!(cls, method) 59 | spies_on = SpiesOnClassMethod.new(cls) 60 | spies_on.raises_no_method_error = false 61 | spies_on.spy(method) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/gimme/gives_class_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'gimme/shared_examples/shared_gives_examples' 3 | 4 | module Gimme 5 | describe GivesClassMethods do 6 | 7 | class Bunny 8 | def self.nibble 9 | end 10 | 11 | def self.eat(food) 12 | end 13 | end 14 | 15 | class Rabbit 16 | def self.eat(food) 17 | end 18 | end 19 | 20 | describe "two classes with similar stubbings" do 21 | Given { give(Rabbit).eat("carrot") { "yum" } } 22 | Given { give(Bunny).eat("carrot") { "yay" } } 23 | Then { Rabbit.eat("carrot").should == "yum" } 24 | Then { Bunny.eat("carrot").should == "yay" } 25 | end 26 | 27 | context "using the class API" do 28 | Given(:subject) { Bunny } 29 | 30 | it_behaves_like "a normal stubbing" do 31 | Given(:gives) { GivesClassMethods.new(subject) } 32 | end 33 | 34 | it_behaves_like "an overridden stubbing" do 35 | Given(:gives) { GivesClassMethods.new(subject) } 36 | end 37 | end 38 | 39 | context "using the gimme DSL" do 40 | Given(:subject) { Bunny } 41 | 42 | it_behaves_like "a normal stubbing" do 43 | Given(:gives) { give(subject) } 44 | end 45 | 46 | it_behaves_like "an overridden stubbing" do 47 | Given(:gives) { give!(subject) } 48 | end 49 | end 50 | 51 | context "when called with a block" do 52 | Given do 53 | give(Rabbit).eat {|blk| blk.call } 54 | end 55 | 56 | 57 | Then do 58 | obj_in_block = false 59 | Rabbit.eat { obj_in_block = true } 60 | obj_in_block.should be_true 61 | end 62 | end 63 | 64 | end 65 | end -------------------------------------------------------------------------------- /features/old/unknown_methods.feature: -------------------------------------------------------------------------------- 1 | Feature: Encountering Unknown Methods 2 | 3 | As a test author 4 | I want my test double to yell at me when I try invoking a method that instances of the class being doubled wouldn't respond to 5 | so that I don't find myself with a green bar and a dependency that can't do what the test thinks it does. 6 | 7 | However, I also want to be able to stub and verify methods that aren't apparently on the class 8 | so that I can test behavior that I'm confident will be added to the test double's real counterpart dynamically at runtime 9 | 10 | Scenario: on a classy double, stubbing an unknown method 11 | Given a new Dog test double 12 | When I stub meow to return "Woof" 13 | Then a NoMethodError is raised 14 | 15 | Scenario: on a classy double, verifying an unknown method 16 | Given a new Dog test double 17 | When I invoke gobbeldy_gook 18 | Then verifying gobbeldy_gook raises a NoMethodError 19 | 20 | Scenario: on a classy double, stubbing a method with "give!" 21 | Given a new Dog test double 22 | When I stub! meow to return :woof 23 | Then invoking meow returns :woof 24 | 25 | Scenario: on a classy double, verifying a method with "verify!" 26 | Given a new Dog test double 27 | When I invoke meow 28 | Then I can verify! meow has been invoked 1 time 29 | 30 | Scenario: on a generic double, stubbing an unknown method 31 | Given a new test double 32 | When I stub meow to return "Woof" 33 | Then no error is raised 34 | 35 | Scenario: on a generic double, verifying an unknown method 36 | Given a new test double 37 | When I invoke gobbeldy_gook 38 | Then I can verify gobbeldy_gook has been invoked 1 time 39 | -------------------------------------------------------------------------------- /spec/gimme/test_double_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Gimme 4 | describe TestDouble do 5 | describe "#gimme_next" do 6 | class MassiveDamage 7 | def boom 8 | :asplode 9 | end 10 | end 11 | 12 | Given(:test_double) { gimme_next(MassiveDamage) } 13 | Given(:subject) { MassiveDamage.new } 14 | 15 | Given { give(test_double).boom { :kaboom } } 16 | When(:result) { subject.boom } 17 | Then { result.should == :kaboom } 18 | 19 | context "subsequent uses" do 20 | Given(:next_subject) { MassiveDamage.new } 21 | When(:next_result) { next_subject.boom } 22 | Then { next_result.should == :asplode } 23 | end 24 | end 25 | 26 | describe "#gimme" do 27 | context "of a class" do 28 | subject { gimme(Object) } 29 | Then { subject.should == subject } 30 | Then { subject.eql?(subject).should == true } 31 | Then { subject.to_s.should == "<#Gimme:1 Object>" } 32 | Then { subject.inspect.should == "<#Gimme:1 Object>" } 33 | Then { {}.tap {|h| h[subject] = subject }[subject].should == subject } 34 | end 35 | 36 | context "with a string name" do 37 | subject { gimme("pants") } 38 | Given { give(subject).name { "pants" } } 39 | Then { subject.to_s.should == "<#Gimme:1 pants>" } 40 | Then { subject.name.should == "pants" } 41 | end 42 | 43 | context "when called with a block" do 44 | subject { gimme } 45 | Given { @number = 1 } 46 | Given { give(subject).process(:three) {|blk| blk.call(3) } } 47 | When { subject.process(:three) { |n| @number += n } } 48 | Then { @number.should == 4 } 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /features/old/stub_basic.feature: -------------------------------------------------------------------------------- 1 | Feature: basic stubbing 2 | 3 | As a test author 4 | I want to create a test double 5 | so that I can stub method returns 6 | 7 | Scenario Outline: stubbing 8 | Given a new Dog test double 9 | When I stub to return 10 | Then invoking returns 11 | 12 | Scenarios: no-arg methods 13 | | method | gives | invocation | returns | 14 | | to_s | 'something' | to_s | 'something' | 15 | | inspect | 'a' | inspect | 'a' | 16 | | hash | 'b' | hash | 'b' | 17 | | to_s | nil | to_s | nil | 18 | | purebred? | true | purebred? | true | 19 | 20 | 21 | Scenarios: one-arg methods 22 | | method | gives | invocation | returns | 23 | | holler_at(true) | :ruff | holler_at(true) | :ruff | 24 | | holler_at(true) | :ruff | holler_at(false) | nil | 25 | | holler_at(true) | :ruff | holler_at(:panda) | nil | 26 | | holler_at(true) | :ruff | holler_at(nil) | nil | 27 | | eql?(:a) | :c | eql?(:a) | :c | 28 | | ==(:b) | :d | ==(:b) | :d | 29 | 30 | Scenarios: two-arg methods 31 | | method | gives | invocation | returns | 32 | | walk_to(1,2) | :park | walk_to(1,2) | :park | 33 | | walk_to(1,2) | :park | walk_to(0.9,2) | nil | 34 | | walk_to(1,2) | :park | walk_to(1,2.1) | nil | 35 | | walk_to([1,5],[2,7]) | :park | walk_to([1],[5,2,7]) | nil | 36 | 37 | Scenario: 38 | Given a new Dog test double 39 | When I stub purebred? to raise StandardError 40 | Then invoking purebred? raises a StandardError -------------------------------------------------------------------------------- /features/step_definitions/doc_steps.rb: -------------------------------------------------------------------------------- 1 | 2 | Given /^we have this production code:$/ do |string| 3 | eval(string) 4 | end 5 | 6 | When /^we want to write some tests to help us write this method:$/ do |string| 7 | eval(string) 8 | end 9 | 10 | When /^we write a test we expect to fail:$/ do |string| 11 | begin 12 | eval(string) 13 | rescue 14 | @last_error = $! 15 | end 16 | 17 | unless @last_error 18 | fail "\nexpected this step's code to raise error, but it did not.\n\n" 19 | end 20 | end 21 | 22 | Then /^we can use gimme to isolate the unit under test:$/ do |string| 23 | eval(string) 24 | end 25 | 26 | Then /^this should work:$/ do |string| 27 | eval(string) 28 | end 29 | 30 | Then /^we should see a failure message that includes:$/ do |string| 31 | fail "expected a prior step to have raised error" unless @last_error 32 | @last_error.message.should =~ Regexp.new(string) 33 | end 34 | 35 | Given /^this RSpec will pass:$/ do |spec_code| 36 | run_spec_for(create_spec_file_for(spec_code)) 37 | end 38 | 39 | def create_spec_file_for(spec_code) 40 | require 'tempfile' 41 | Tempfile.new('spec').tap do |file| 42 | file.write <<-RUBY 43 | require 'rspec' 44 | require 'rspec/given' 45 | require 'gimme' 46 | 47 | #{spec_code} 48 | RUBY 49 | file.close 50 | end 51 | end 52 | 53 | class Output 54 | attr_reader :output 55 | def initialize 56 | @output = "" 57 | end 58 | def puts(stuff="") 59 | @output += stuff + "\n" 60 | end 61 | def print(stuff="") 62 | @output += stuff 63 | end 64 | end 65 | 66 | def run_spec_for(file) 67 | require 'rspec' 68 | out = Output.new 69 | unless RSpec::Core::Runner.run([file.path], out, out) == 0 70 | fail <<-RSPEC 71 | *********************************** 72 | RSpec execution failed with output: 73 | *********************************** 74 | 75 | #{out.output} 76 | RSPEC 77 | end 78 | end -------------------------------------------------------------------------------- /lib/gimme/test_double.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | class BlankSlate 3 | warning_level = $VERBOSE 4 | $VERBOSE = nil 5 | instance_methods.each { |m| undef_method m unless m =~ /^__/ } 6 | $VERBOSE = warning_level 7 | end 8 | 9 | class TestDouble < BlankSlate 10 | attr_accessor :cls 11 | 12 | @@gimme_count = 0 13 | Gimme.on_reset(:every) { @@gimme_count = 0 } 14 | 15 | def initialize(cls_or_name=nil) 16 | @name = cls_or_name 17 | @cls = cls_or_name if cls_or_name.kind_of?(Class) 18 | @gimme_id = (@@gimme_count += 1) 19 | end 20 | 21 | def method_missing(method, *args, &block) 22 | method = ResolvesMethods.new(self.cls, method, args).resolve(false) 23 | Gimme.invocations.increment(self, method, args) 24 | InvokesSatisfiedStubbing.new(self).invoke(method, args, block) 25 | end 26 | 27 | def inspect(*args, &blk) 28 | if stubbed?(:inspect, *args, &blk) 29 | method_missing(:inspect, *args, &blk) 30 | else 31 | "<#Gimme:#{@gimme_id} #{@name}>" 32 | end 33 | end 34 | 35 | def to_s(*args, &blk) 36 | if stubbed?(:to_s, *args, &blk) 37 | method_missing(:to_s, *args, &blk) 38 | else 39 | inspect 40 | end 41 | end 42 | 43 | def hash(*args, &blk) 44 | if stubbed?(:hash, *args, &blk) 45 | method_missing(:hash, *args, &blk) 46 | else 47 | __id__ 48 | end 49 | end 50 | 51 | def eql?(other, *args, &blk) 52 | if stubbed?(:eql?, other, *args, &blk) 53 | method_missing(:eql?, other, *args, &blk) 54 | else 55 | __id__ == other.__id__ 56 | end 57 | end 58 | 59 | def ==(other, *args, &blk) 60 | if stubbed?(:==, other, *args, &blk) 61 | method_missing(:==, other, *args, &blk) 62 | else 63 | eql?(other) 64 | end 65 | end 66 | 67 | private 68 | def stubbed?(method, *args, &blk) 69 | FindsStubbings.new(self).count(method, args) > 0 70 | end 71 | end 72 | 73 | end 74 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options 8 | gem.name = "gimme" 9 | gem.homepage = "http://github.com/searls/gimme" 10 | gem.license = "MIT" 11 | gem.summary = %Q{gimme — a low-specification test double library for Ruby} 12 | gem.description = %Q{gimme attempts to bring to Ruby a test double workflow akin to Mockito in Java. Major distinctions include preserving arrange-act-assert in tests, fast feedback for methods the double's real counterpart may not know how to respond to, no string/symbolic representations of methods, argument captors, and strong opinions (weakly held). } 13 | gem.email = "searls@gmail.com" 14 | gem.authors = ["Justin Searls"] 15 | # Include your dependencies below. Runtime dependencies are required when using your gem, 16 | # and development dependencies are only needed for development (ie running rake tasks, tests, etc) 17 | # gem.add_runtime_dependency 'jabber4r', '> 0.1' 18 | gem.add_development_dependency "rspec", ">= 1.3.1" 19 | gem.add_development_dependency "cucumber", ">= 0.10.0" 20 | end 21 | Jeweler::RubygemsDotOrgTasks.new 22 | rescue LoadError 23 | puts "`gem install jeweler` and run outside of bundler to do jeweler stuff. You can thank its dependency on bundler ~> 1.0.0 for this hack." 24 | end 25 | 26 | require 'rdoc/task' 27 | RDoc::Task.new do |rdoc| 28 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 29 | 30 | rdoc.rdoc_dir = 'rdoc' 31 | rdoc.title = "gimme #{version}" 32 | rdoc.rdoc_files.include('README*') 33 | rdoc.rdoc_files.include('lib/**/*.rb') 34 | end 35 | 36 | require 'cucumber/rake/task' 37 | Cucumber::Rake::Task.new do |t| 38 | t.cucumber_opts = %w{--format progress} 39 | end 40 | 41 | Cucumber::Rake::Task.new('cucumber:wip') do |t| 42 | t.cucumber_opts = %w{--format progress --wip --tags @wip} 43 | end 44 | 45 | 46 | require 'rspec/core/rake_task' 47 | RSpec::Core::RakeTask.new 48 | 49 | task :default => [:spec,:cucumber] 50 | -------------------------------------------------------------------------------- /spec/gimme/spies_on_class_method_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Gimme 4 | class Factory 5 | 6 | def self.inherited_build 7 | raise RuntimeError.new "unimplemented feature" 8 | end 9 | 10 | end 11 | 12 | class ChairFactory < Factory 13 | 14 | def self.build 15 | raise RuntimeError.new "unimplemented feature" 16 | end 17 | 18 | def self.destroy 19 | raise RuntimeError.new "unimplemented feature" 20 | end 21 | 22 | end 23 | 24 | describe SpiesOnClassMethod do 25 | shared_examples_for "it spies on class methods" do 26 | describe "normal class method spy" do 27 | Given(:invocation) { lambda { ChairFactory.build } } 28 | Then { invocation.should_not raise_error } 29 | 30 | context "upon reset" do 31 | When { Gimme.reset } 32 | Then { invocation.should raise_error } 33 | end 34 | end 35 | 36 | describe "inherited class method spy" do 37 | Given(:invocation) { lambda { ChairFactory.inherited_build } } 38 | Then { invocation.should_not raise_error } 39 | 40 | context "upon reset" do 41 | When { Gimme.reset } 42 | Then { invocation.should raise_error } 43 | end 44 | end 45 | 46 | describe "imaginary class method spy" do 47 | Given(:invocation) { lambda { ChairFactory.fantasy } } 48 | Then { invocation.should_not raise_error } 49 | 50 | context "upon reset" do 51 | When { Gimme.reset } 52 | Then { invocation.should raise_error } 53 | end 54 | end 55 | end 56 | 57 | context "classical API" do 58 | it_behaves_like "it spies on class methods" do 59 | subject { SpiesOnClassMethod.new(ChairFactory) } 60 | Given { subject.spy(:build) } 61 | Given { subject.spy(:inherited_build) } 62 | Given { subject.raises_no_method_error = false } 63 | Given { subject.spy(:fantasy) } 64 | end 65 | end 66 | 67 | context "gimme DSL" do 68 | it_behaves_like "it spies on class methods" do 69 | Given { spy_on(ChairFactory, :build) } 70 | Given { spy_on(ChairFactory, :inherited_build) } 71 | Given { spy_on!(ChairFactory, :fantasy) } 72 | end 73 | end 74 | 75 | 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/gimme/resolves_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class Berry 4 | def ferment 5 | end 6 | 7 | private 8 | 9 | def grow 10 | end 11 | end 12 | 13 | module Gimme 14 | describe ResolvesMethods do 15 | describe "#resolve" do 16 | context "no class on the double" do 17 | context "plain ol' method" do 18 | subject { ResolvesMethods.new(nil,:foo,[]) } 19 | Then { subject.resolve.should == :foo } 20 | end 21 | 22 | context "using send" do 23 | subject { ResolvesMethods.new(nil,:send,[:foo]) } 24 | Then { subject.resolve.should == :foo } 25 | end 26 | end 27 | 28 | context "a class is provided" do 29 | context "rigged to raise errors" do 30 | context "method exists" do 31 | subject { ResolvesMethods.new(Berry,:ferment,[]) } 32 | Then { subject.resolve.should == :ferment } 33 | end 34 | 35 | context "method does not exist" do 36 | subject { ResolvesMethods.new(Berry,:fooberry,[]) } 37 | When(:invocation) { lambda { subject.resolve } } 38 | Then { invocation.should raise_error NoMethodError, /may not know how to respond/ } 39 | end 40 | 41 | context "a private method" do 42 | subject { ResolvesMethods.new(Berry,:grow,[]) } 43 | When(:invocation) { lambda { subject.resolve } } 44 | Then { invocation.should raise_error NoMethodError, /not be a great idea/ } 45 | end 46 | 47 | end 48 | 49 | context "set up not to raise errors" do 50 | context "method exists" do 51 | subject { ResolvesMethods.new(Berry,:ferment,[]) } 52 | When(:result) { subject.resolve(false) } 53 | Then { result.should == :ferment } 54 | end 55 | 56 | context "method does not exist" do 57 | subject { ResolvesMethods.new(Berry,:fooberry,[]) } 58 | When(:result) { subject.resolve(false) } 59 | Then { result.should == :fooberry } 60 | end 61 | 62 | context "a private methods" do 63 | subject { ResolvesMethods.new(Berry,:grow,[]) } 64 | When(:result) { subject.resolve(false) } 65 | Then { result.should == :grow } 66 | end 67 | end 68 | 69 | end 70 | 71 | end 72 | end 73 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | builder (3.0.0) 5 | coderay (1.0.5) 6 | colorize (0.5.8) 7 | coveralls (0.6.7) 8 | colorize 9 | multi_json (~> 1.3) 10 | rest-client 11 | simplecov (>= 0.7) 12 | thor 13 | cucumber (1.1.9) 14 | builder (>= 2.1.2) 15 | diff-lcs (>= 1.1.2) 16 | gherkin (~> 2.9.0) 17 | json (>= 1.4.6) 18 | term-ansicolor (>= 1.0.6) 19 | diff-lcs (1.1.3) 20 | domain_name (0.5.20190701) 21 | unf (>= 0.0.5, < 1.0.0) 22 | ffi (1.11.1) 23 | gherkin (2.9.0) 24 | json (>= 1.4.6) 25 | git (1.2.5) 26 | guard (1.0.0) 27 | ffi (>= 0.5.0) 28 | thor (~> 0.14.6) 29 | guard-cucumber (0.7.5) 30 | cucumber (>= 0.10) 31 | guard (>= 0.8.3) 32 | guard-rspec (0.6.0) 33 | guard (>= 0.10.0) 34 | http-accept (1.7.0) 35 | http-cookie (1.0.3) 36 | domain_name (~> 0.5) 37 | jeweler (1.8.3) 38 | bundler (~> 1.0) 39 | git (>= 1.2.5) 40 | rake 41 | rdoc 42 | json (1.8.6) 43 | method_source (0.7.0) 44 | mime-types (3.3) 45 | mime-types-data (~> 3.2015) 46 | mime-types-data (3.2019.1009) 47 | multi_json (1.7.3) 48 | netrc (0.11.0) 49 | pry (0.9.8) 50 | coderay (~> 1.0.5) 51 | method_source (~> 0.7) 52 | slop (>= 2.4.3, < 3) 53 | rake (0.9.2.2) 54 | rdoc (3.12.1) 55 | json (~> 1.4) 56 | rest-client (2.1.0) 57 | http-accept (>= 1.7.0, < 2.0) 58 | http-cookie (>= 1.0.2, < 2.0) 59 | mime-types (>= 1.16, < 4.0) 60 | netrc (~> 0.8) 61 | rspec (2.8.0) 62 | rspec-core (~> 2.8.0) 63 | rspec-expectations (~> 2.8.0) 64 | rspec-mocks (~> 2.8.0) 65 | rspec-core (2.8.0) 66 | rspec-expectations (2.8.0) 67 | diff-lcs (~> 1.1.2) 68 | rspec-given (1.4.2) 69 | rspec (> 1.2.8) 70 | rspec-mocks (2.8.0) 71 | simplecov (0.7.1) 72 | multi_json (~> 1.0) 73 | simplecov-html (~> 0.7.1) 74 | simplecov-html (0.7.1) 75 | slop (2.4.3) 76 | term-ansicolor (1.0.7) 77 | thor (0.14.6) 78 | unf (0.1.4) 79 | unf_ext 80 | unf_ext (0.0.7.6) 81 | 82 | PLATFORMS 83 | ruby 84 | 85 | DEPENDENCIES 86 | coveralls 87 | cucumber 88 | guard-cucumber 89 | guard-rspec 90 | jeweler 91 | pry 92 | rake 93 | rdoc 94 | rspec 95 | rspec-given 96 | simplecov 97 | -------------------------------------------------------------------------------- /features/basics.feature: -------------------------------------------------------------------------------- 1 | Feature: basic usage 2 | 3 | Gimme is designed for easy & reliable isolated unit testing of Ruby classes. 4 | 5 | This example illustrates: 6 |
    7 |
  • How to create a test double for a known class
  • 8 |
  • Injecting that double into the subject code we're specifying
  • 9 |
  • Stubbing a method on a test double to return a particular result
  • 10 |
  • Verify that a method on a test double was called with a particular argument
  • 11 |
12 | 13 | So say we've got a Chef who's job is to take the work product of his Apprentice 14 | and simmer it on the Stove. Here's how we might use gimme to write an isolation test 15 | of the Chef's job without actually calling through to a real Apprentice or a real Stove. 16 | 17 | Scenario: 18 | Given we have this production code: 19 | """ 20 | class Apprentice 21 | def slice(thing) 22 | 1000000.times.map do 23 | Slice.new(thing) 24 | end 25 | end 26 | end 27 | 28 | class Stove 29 | def simmer(stuff) 30 | end 31 | end 32 | 33 | class Slice 34 | def initialize(of) 35 | end 36 | end 37 | 38 | class Chef 39 | def initialize(slicer = Apprentice.new, stove = Stove.new) 40 | @slicer = slicer 41 | @stove = stove 42 | end 43 | 44 | def cook 45 | slices = @slicer.slice("tomato") 46 | @stove.simmer(slices) 47 | end 48 | end 49 | 50 | """ 51 | Then this RSpec will pass: 52 | """ 53 | describe Chef do 54 | describe "#cook" do 55 | Given!(:slicer) { gimme_next(Apprentice) } 56 | Given!(:stove) { gimme_next(Stove) } 57 | Given { give(slicer).slice("tomato") { "some slices" } } 58 | When { subject.cook } 59 | Then { verify(stove).simmer("some slices") } 60 | end 61 | end 62 | """ 63 | 64 | Scenario: using rspec 65 | Given we have this production code: 66 | """ 67 | class Spaceship 68 | def initialize(thruster = Thruster.new) 69 | @thruster = thruster 70 | end 71 | 72 | def take_off 73 | @thruster.fire 74 | end 75 | end 76 | 77 | class Thruster 78 | def fire 79 | raise "LOLTHRUSTER" 80 | end 81 | end 82 | """ 83 | Then this RSpec will pass: 84 | """ 85 | describe Spaceship do 86 | context "an injected double" do 87 | Given(:thruster) { gimme(Thruster) } 88 | subject { Spaceship.new(thruster) } 89 | When { subject.take_off } 90 | Then { verify(thruster).fire } 91 | end 92 | 93 | context "a gimme_next double" do 94 | Given!(:thruster) { gimme_next(Thruster) } 95 | When { subject.take_off } 96 | Then { verify(thruster).fire } 97 | end 98 | end 99 | """ 100 | -------------------------------------------------------------------------------- /features/old/stub_matchers.feature: -------------------------------------------------------------------------------- 1 | Feature: stubbing with matchers 2 | 3 | As a test author 4 | I want to be able to stub an invocation based on matchers' evaluations of the arguments 5 | so that I don't need to redundantly stub things I don't care about 6 | 7 | Scenario Outline: stubbing 8 | Given a new Dog test double 9 | When I stub to return 10 | Then invoking returns 11 | 12 | Scenarios: the anything matcher with a one-argument method 13 | | method | gives | invocation | returns | 14 | | introduce_to(anything) | 'Why Hello!' | introduce_to(Cat.new) | 'Why Hello!' | 15 | | introduce_to(anything) | 'Why Hello!' | introduce_to(Dog.new) | 'Why Hello!' | 16 | | introduce_to(anything) | 'Why Hello!' | introduce_to(nil) | 'Why Hello!' | 17 | 18 | Scenarios: the anything matcher with a two-argument method 19 | | method | gives | invocation | returns | 20 | | walk_to(anything,5) | 'Park' | walk_to(5,5) | 'Park' | 21 | | walk_to(anything,5) | 'Park' | walk_to('pants',5) | 'Park' | 22 | | walk_to(anything,5) | 'Park' | walk_to(nil,5) | 'Park' | 23 | | walk_to(anything,5) | 'Park' | walk_to(3,5.1) | nil | 24 | | walk_to(3.123,anything) | 'Park' | walk_to(3.123,nil) | 'Park' | 25 | | walk_to(anything,anything) | 'Park' | walk_to(3,5.1) | 'Park' | 26 | 27 | Scenarios: the anything matcher with a variable-argument method (argument size must match; but I don't know if I like some of these…) 28 | | method | gives | invocation | returns | 29 | | eat(anything,:fish,anything) | :yum | eat(:cat,:fish,:mouse) | :yum | 30 | | eat(anything,:fish,anything) | :yum | eat(:cat,:pants,:mouse) | nil | 31 | | eat(anything) | :yum | eat(:cat,:pants) | nil | 32 | | eat(:cat,anything) | :yum | eat(:cat) | nil | 33 | | eat(:cat,anything) | :yum | eat(:cat,nil) | :yum | 34 | 35 | Scenarios: the is_a matcher 36 | | method | gives | invocation | returns | 37 | | introduce_to(is_a(Animal)) | :howdy | introduce_to(Animal.new) | :howdy | 38 | | introduce_to(is_a(Animal)) | :howdy | introduce_to(Cat.new) | :howdy | 39 | | introduce_to(is_a(Animal)) | :howdy | introduce_to(Object.new) | nil | 40 | | introduce_to(is_a(Animal)) | :howdy | introduce_to(nil) | nil | 41 | 42 | Scenarios: the any matcher (like is_a but also matches nil) 43 | | method | gives | invocation | returns | 44 | | introduce_to(any(Animal)) | :howdy | introduce_to(Animal.new) | :howdy | 45 | | introduce_to(any(Animal)) | :howdy | introduce_to(Cat.new) | :howdy | 46 | | introduce_to(any(Animal)) | :howdy | introduce_to(Object.new) | nil | 47 | | introduce_to(any(Animal)) | :howdy | introduce_to(nil) | :howdy | 48 | 49 | Scenarios: the numeric matcher 50 | | method | gives | invocation | returns | 51 | | walk_to(numeric,numeric) | :hydrant | walk_to(1.498,8) | :hydrant | 52 | | walk_to(numeric,numeric) | :hydrant | walk_to(1.498,'string') | nil | 53 | | walk_to(numeric,numeric) | :hydrant | walk_to(1.498,nil) | nil | 54 | 55 | Scenarios: the boolean matcher 56 | | method | gives | invocation | returns | 57 | | holler_at(boolean) | :ruff | holler_at(true) | :ruff | 58 | | holler_at(boolean) | :ruff | holler_at(false) | :ruff | 59 | | holler_at(boolean) | :ruff | holler_at('woof') | nil | 60 | | holler_at(boolean) | :ruff | holler_at(nil) | nil | 61 | -------------------------------------------------------------------------------- /spec/gimme/matchers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec::Matchers.define :match do |expected| 4 | match do |actual| 5 | actual.matches?(expected) 6 | end 7 | end 8 | 9 | module Gimme 10 | describe Matchers do 11 | class Shoopuf 12 | end 13 | 14 | class BabyShoopuf < Shoopuf 15 | end 16 | 17 | describe Matcher do 18 | context "a plain, default matcher" do 19 | Given(:matcher) { Matchers::Matcher.new } 20 | Then { matcher.should_not match "anything" } 21 | end 22 | end 23 | 24 | describe Anything do 25 | shared_examples_for "an anything matcher" do 26 | Then { matcher.should match "anything" } 27 | end 28 | 29 | context "class API" do 30 | it_behaves_like "an anything matcher" do 31 | Given(:matcher) { Anything.new } 32 | end 33 | end 34 | 35 | context "gimme DSL" do 36 | it_behaves_like "an anything matcher" do 37 | Given(:matcher) { anything } 38 | end 39 | end 40 | end 41 | 42 | shared_examples_for "an identity matcher" do 43 | Then { matcher.should match Shoopuf.new } 44 | Then { matcher.should match BabyShoopuf.new } 45 | end 46 | 47 | shared_examples_for "a restrictive matcher" do 48 | Then { matcher.should_not match nil } 49 | Then { matcher.should_not match "Pandas" } 50 | Then { matcher.should_not match Object.new } 51 | Then { matcher.should_not match Class } 52 | end 53 | 54 | shared_examples_for "a relaxed matcher" do 55 | Then { matcher.should match nil } 56 | end 57 | 58 | 59 | describe IsA do 60 | context "class API" do 61 | Given(:matcher) { IsA.new(Shoopuf) } 62 | it_behaves_like "an identity matcher" 63 | it_behaves_like "a restrictive matcher" 64 | end 65 | 66 | context "gimme DSL" do 67 | Given(:matcher) { is_a(Shoopuf) } 68 | it_behaves_like "an identity matcher" 69 | it_behaves_like "a restrictive matcher" 70 | end 71 | end 72 | 73 | describe Any do 74 | context "class API" do 75 | Given(:matcher) { Any.new(Shoopuf) } 76 | it_behaves_like "an identity matcher" 77 | it_behaves_like "a relaxed matcher" 78 | end 79 | 80 | context "gimme DSL" do 81 | Given(:matcher) { any(Shoopuf) } 82 | it_behaves_like "an identity matcher" 83 | it_behaves_like "a relaxed matcher" 84 | end 85 | end 86 | 87 | describe Matchers::Numeric do 88 | shared_examples_for "a numeric matcher" do 89 | Then { matcher.should match 5 } 90 | Then { matcher.should match 5.5 } 91 | end 92 | 93 | context "class API" do 94 | Given(:matcher) { Matchers::Numeric.new } 95 | it_behaves_like "a numeric matcher" 96 | it_behaves_like "a restrictive matcher" 97 | end 98 | 99 | context "gimme DSL" do 100 | Given(:matcher) { numeric } 101 | it_behaves_like "a numeric matcher" 102 | it_behaves_like "a restrictive matcher" 103 | end 104 | end 105 | 106 | describe Matchers::Boolean do 107 | shared_examples_for "a boolean matcher" do 108 | Then { matcher.should match true } 109 | Then { matcher.should match false } 110 | Then { matcher.should_not match Boolean } 111 | end 112 | 113 | context "class API" do 114 | Given(:matcher) { Matchers::Boolean.new } 115 | it_behaves_like "a boolean matcher" 116 | it_behaves_like "a restrictive matcher" 117 | end 118 | 119 | context "gimme DSL" do 120 | Given(:matcher) { boolean } 121 | it_behaves_like "a boolean matcher" 122 | it_behaves_like "a restrictive matcher" 123 | end 124 | end 125 | end 126 | end -------------------------------------------------------------------------------- /spec/gimme/shared_examples/shared_verifies_examples.rb: -------------------------------------------------------------------------------- 1 | module Gimme 2 | shared_examples_for "a verifier" do 3 | 4 | context "invoked once when expected once" do 5 | Given { test_double.ferment } 6 | When(:result) { lambda { verifier.ferment } } 7 | Then { result.should_not raise_error } 8 | end 9 | 10 | context "using the N.times syntax for invocation count" do 11 | Given { 2.times { test_double.ferment } } 12 | Given(:verifier) { verifier_class.new(test_double, 2.times) } 13 | When(:result) { lambda { 2.times { verifier.ferment } } } 14 | Then { result.should_not raise_error } 15 | end 16 | 17 | context "never invoked" do 18 | When(:result) { lambda { verifier.ferment } } 19 | Then { result.should raise_error Errors::VerificationFailedError } 20 | Then do result.should raise_error Errors::VerificationFailedError, 21 | "expected #{double_name}#ferment to have been called with arguments #{[]}\n"+ 22 | " but was never called" 23 | end 24 | end 25 | 26 | context "invoked with incorrect args" do 27 | Given { test_double.ferment(5) } 28 | When(:result) { lambda { verifier.ferment(4) } } 29 | Then do result.should raise_error Errors::VerificationFailedError, 30 | "expected #{double_name}#ferment to have been called with arguments #{[4]}\n"+ 31 | " was actually called 1 times with arguments #{[5]}" 32 | end 33 | end 34 | 35 | context "invoked incorrectly a whole bunch" do 36 | Given { test_double.ferment(5) } 37 | Given { test_double.ferment(5) } 38 | Given { test_double.ferment(3) } 39 | When(:result) { lambda { verifier.ferment(4) } } 40 | Then do result.should raise_error Errors::VerificationFailedError, 41 | /.* was actually called 2 times with arguments #{Regexp.escape([5].to_s)}.*/m 42 | end 43 | Then do result.should raise_error Errors::VerificationFailedError, 44 | /.* was actually called 1 times with arguments #{Regexp.escape([3].to_s)}.*/m 45 | end 46 | end 47 | 48 | context "invoked too few times" do 49 | Given(:verifier) { verifier_class.new(test_double,3) } 50 | Given { 2.times { test_double.ferment } } 51 | When(:result) { lambda { verifier.ferment } } 52 | Then { result.should raise_error Errors::VerificationFailedError } 53 | end 54 | 55 | context "juggling multiple verifiers for the same method" do 56 | Given(:multi_verifier) { verifier_class.new(test_double,2) } 57 | Given { test_double.ferment(:panda,:sauce) } 58 | Given { 2.times { test_double.ferment(2,3) } } 59 | When(:result) do 60 | lambda do 61 | multi_verifier.ferment(2,3) 62 | verifier.ferment(:panda,:sauce) 63 | end 64 | end 65 | Then { result.should_not raise_error } 66 | end 67 | 68 | context "a method not on the test_double" do 69 | When(:result) { lambda { verifier.eat } } 70 | Then { result.should raise_error NoMethodError } 71 | end 72 | 73 | context "a satisfied argument matcher" do 74 | Given { test_double.ferment(5) } 75 | When(:result) { lambda { verifier.ferment(numeric) } } 76 | Then { result.should_not raise_error } 77 | end 78 | 79 | context "an unsatisifed argument matcher" do 80 | Given { test_double.ferment("True") } 81 | When(:result) { lambda { verifier.ferment(boolean) } } 82 | Then { result.should raise_error Errors::VerificationFailedError } 83 | end 84 | end 85 | 86 | shared_examples_for "an overridden verifier" do 87 | context "a method not on the double that is invoked" do 88 | Given { test_double.eat } 89 | When(:result) { lambda { verifier.eat } } 90 | Then { result.should_not raise_error } 91 | end 92 | 93 | context "a method not on the test_double that is _not_ invoked" do 94 | When(:result) { lambda { verifier.eat } } 95 | Then { result.should raise_error Errors::VerificationFailedError } 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /features/step_definitions/gimme_steps.rb: -------------------------------------------------------------------------------- 1 | include Gimme 2 | METHOD_PATTERN = /([^\(]*)(\(.*\))?/ 3 | 4 | # Creating 5 | 6 | Given /^a new (.*)\s?test double$/ do | type | 7 | @double = type.empty? ? gimme : gimme(eval(type)) 8 | end 9 | 10 | Given /^I create a double via gimme_next\((.*)\)$/ do |klass| 11 | @double = gimme_next(eval(klass)) 12 | end 13 | 14 | 15 | # Stubbing 16 | 17 | When /^I stub #{METHOD_PATTERN} to return (.*)$/ do |method,args,result| 18 | send_and_trap_error(NoMethodError,give(@double),method,args,result) 19 | end 20 | 21 | When /^I stub! #{METHOD_PATTERN} to return (.*)$/ do |method,args,result| 22 | send_and_trap_error(NoMethodError,give!(@double),method,args,result) 23 | end 24 | 25 | When /^I stub #{METHOD_PATTERN} to raise (.*)$/ do |method,args,error_type| 26 | sendish(give(@double),method,args,"raise #{error_type}") 27 | end 28 | 29 | ## class stubbing 30 | Given /^the (.*) class$/ do |cls| 31 | @double = eval(cls) 32 | end 33 | 34 | When /^I reset Gimme$/ do 35 | Gimme.reset 36 | end 37 | 38 | Then /^#{METHOD_PATTERN} no longer returns (.*)$/ do |method,args,result| 39 | sendish(@double,method,args).should_not == eval(result) 40 | end 41 | 42 | 43 | # stubbing invocation 44 | 45 | Then /^invoking #{METHOD_PATTERN} returns (.*)$/ do |method,args,result| 46 | sendish(@double,method,args).should == eval(result) 47 | end 48 | 49 | When /^I invoke #{METHOD_PATTERN}$/ do |method,args| 50 | sendish(@double,method,args) 51 | end 52 | 53 | Given /^I do not invoke #{METHOD_PATTERN}$/ do |method,args| 54 | end 55 | 56 | Then /^invoking (.*) raises a (.*)$/ do |method,error_type| 57 | expect_error(eval(error_type)) { sendish(@double,method) } 58 | end 59 | 60 | # Verifying 61 | 62 | Then /^verifying #{METHOD_PATTERN} raises a (.*)$/ do |method,args,error_type| 63 | expect_error(eval(error_type)) { verify(@double).send(method.to_sym) } 64 | end 65 | 66 | Then /^I can verify #{METHOD_PATTERN} has been invoked$/ do |method,args| 67 | sendish(verify(@double),method,args) 68 | end 69 | 70 | Then /^I can verify #{METHOD_PATTERN} has been invoked (\d+) times?$/ do |method,args,times| 71 | sendish(verify(@double,times.to_i),method,args) 72 | end 73 | 74 | Then /^I can verify! #{METHOD_PATTERN} has been invoked (\d+) times?$/ do |method,args,times| 75 | sendish(verify!(@double,times.to_i),method,args) 76 | end 77 | 78 | When /^I spy on ([^.]*)\.(.*)$/ do |cls, method| 79 | spy_on(eval(cls), method.to_sym) 80 | end 81 | 82 | Then /^it can verify no\-arg methods too\.$/ do 83 | step "I do not invoke yawn" 84 | step "verifying yawn raises a Gimme::Errors::VerificationFailedError" 85 | step "I can verify yawn has been invoked 0 times" 86 | 87 | step "I invoke yawn" 88 | step "I can verify yawn has been invoked" 89 | step "I can verify yawn has been invoked 1 time" 90 | 91 | step "I invoke yawn" 92 | step "I can verify yawn has been invoked 2 times" 93 | end 94 | 95 | #Captors 96 | 97 | Given /^a new argument captor$/ do 98 | @captor = Captor.new 99 | end 100 | 101 | Then /^the captor's value is (.*)$/ do |value| 102 | @captor.value.should == eval(value) 103 | end 104 | 105 | # Exceptions 106 | Then /^a (.*) is raised$/ do |error_type| 107 | @error.should be_a_kind_of eval(error_type) 108 | @error = nil 109 | end 110 | 111 | Then /^no error is raised$/ do 112 | @error.should be nil 113 | end 114 | 115 | # Gimme Next 116 | 117 | When /^my SUT tries creating a real (.*)$/ do |instantiation| 118 | @real = eval(instantiation) 119 | end 120 | 121 | Then /^both the double and real object reference the same object$/ do 122 | @real.__id__ == @double.__id__ 123 | end 124 | 125 | # private 126 | 127 | def send_and_trap_error(error_type,target,method,args=nil,result=nil) 128 | begin 129 | sendish(target,method,args,result) 130 | rescue error_type => e 131 | @error = e 132 | end 133 | end 134 | 135 | def sendish(target,method,args=nil,result=nil) 136 | s = "target.#{method}#{args}" 137 | s += " { #{result} }" if result 138 | eval(s) 139 | end 140 | 141 | def expect_error(type,&block) 142 | rescued = false 143 | begin 144 | yield 145 | rescue type 146 | rescued = true 147 | end 148 | rescued.should be true 149 | end 150 | -------------------------------------------------------------------------------- /features/matchers.feature: -------------------------------------------------------------------------------- 1 | Feature: argument matchers 2 | 3 | By default, gimme will only stub and verify methods that are called with the same arguments 4 | in your spec and your production code. But gimme also includes a number of argument matchers 5 | that allow more flexible specifications, particularly when you don't care about or can't 6 | know the exact arguments passed to a method on a test double. 7 | 8 | This example illustrates: 9 |
    10 |
  • How to use an argument matcher when stubbing a method
  • 11 |
  • Using argument matchers when verifying a method
  • 12 |
  • Using argument captors, a special sort of matcher
  • 13 |
  • Defining your own matcher
  • 14 |
15 | 16 | In this example, we have a Mail object with a contents attribute. A DeliversMessages object adds Mail to 17 | Recipients' mailboxes and checks off each delivery on its Checklist. 18 | 19 | 20 | Background: 21 | Given we have this production code: 22 | """ 23 | class Mail 24 | attr_reader :contents 25 | def initialize(contents) 26 | @contents = contents 27 | end 28 | end 29 | 30 | class Recipient 31 | def add_to_mailbox(thing) 32 | end 33 | end 34 | 35 | class Checklist 36 | def check_off(message_summary, recipient, timestamp) 37 | end 38 | end 39 | 40 | class DeliversMessages 41 | def initialize(checklist = Checklist.new) 42 | @checklist = checklist 43 | end 44 | end 45 | """ 46 | When we want to write some tests to help us write this method: 47 | """ 48 | class DeliversMessages 49 | def deliver(message, recipient = Recipient.new) 50 | mail = Mail.new(message) 51 | recipient.add_to_mailbox(mail) 52 | @checklist.check_off(message[0..4], recipient, Time.now) 53 | end 54 | end 55 | """ 56 | 57 | Scenario: stubbing with argument matchers 58 | Then we can use gimme to isolate the unit under test: 59 | """ 60 | # pretend we want to ensure the #deliver function returns 61 | # whatever the Checlist#check_off method returns. We can 62 | # specify that by stubbing the check_off method 63 | checklist = gimme(Checklist) 64 | recipient = gimme(Recipient) 65 | give(checklist).check_off(anything, anything, is_a(Time)) { "Pandas!" } 66 | 67 | result = DeliversMessages.new(checklist).deliver("WHY HELLO GOOD SIR", recipient) 68 | 69 | result.should == "Pandas!" 70 | """ 71 | 72 | Scenario: using some built-in matchers 73 | Then we can use gimme to isolate the unit under test: 74 | """ 75 | checklist = gimme(Checklist) 76 | recipient = gimme(Recipient) 77 | 78 | DeliversMessages.new(checklist).deliver("WHY HELLO GOOD SIR", recipient) 79 | 80 | verify(recipient).add_to_mailbox(is_a(Mail)) #would force a Mail instance 81 | verify(recipient).add_to_mailbox(any(Mail)) #would also have allowed nil 82 | 83 | verify(checklist).check_off(any(String), recipient, anything) #anything matches anything 84 | 85 | """ 86 | 87 | Scenario: using an argument captor to grab what was passed 88 | Then we can use gimme to isolate the unit under test: 89 | """ 90 | checklist = gimme(Checklist) 91 | recipient = gimme(Recipient) 92 | 93 | DeliversMessages.new(checklist).deliver("WHY HELLO GOOD SIR", recipient) 94 | 95 | mail_captor = Gimme::Captor.new 96 | verify(recipient).add_to_mailbox(capture(mail_captor)) 97 | mail_captor.value.contents.should == "WHY HELLO GOOD SIR" 98 | """ 99 | 100 | Scenario: writing a custom starts_with matcher 101 | Then we can use gimme to isolate the unit under test: 102 | """ 103 | checklist = gimme(Checklist) 104 | recipient = gimme(Recipient) 105 | 106 | DeliversMessages.new(checklist).deliver("WHY HELLO GOOD SIR", recipient) 107 | 108 | class StartsWith 109 | def initialize(expected) 110 | @expected = expected 111 | end 112 | 113 | def matches?(actual) 114 | actual.start_with?(@expected) 115 | end 116 | end 117 | def starts_with(s) 118 | StartsWith.new(s) 119 | end 120 | 121 | verify(checklist).check_off(starts_with("WHY"), anything, anything) 122 | """ -------------------------------------------------------------------------------- /gimme.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "gimme" 8 | s.version = "0.5.0" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Justin Searls"] 12 | s.date = "2014-02-02" 13 | s.description = "gimme attempts to bring to Ruby a test double workflow akin to Mockito in Java. Major distinctions include preserving arrange-act-assert in tests, fast feedback for methods the double's real counterpart may not know how to respond to, no string/symbolic representations of methods, argument captors, and strong opinions (weakly held). " 14 | s.email = "searls@gmail.com" 15 | s.extra_rdoc_files = [ 16 | "LICENSE.txt", 17 | "README.markdown", 18 | "README.rdoc" 19 | ] 20 | s.files = [ 21 | ".document", 22 | ".travis.yml", 23 | "Gemfile", 24 | "Gemfile.lock", 25 | "Guardfile", 26 | "LICENSE.txt", 27 | "README.markdown", 28 | "README.rdoc", 29 | "Rakefile", 30 | "VERSION", 31 | "features/basics.feature", 32 | "features/class_methods.feature", 33 | "features/matchers.feature", 34 | "features/messages.feature", 35 | "features/old/argument_captors.feature", 36 | "features/old/gimme_next.feature", 37 | "features/old/stub_basic.feature", 38 | "features/old/stub_class_methods.feature", 39 | "features/old/stub_matchers.feature", 40 | "features/old/stub_sensible_defaults.feature", 41 | "features/old/unknown_methods.feature", 42 | "features/old/verify_matcher_anything.feature", 43 | "features/old/verify_no_args.feature", 44 | "features/old/verify_with_args.feature", 45 | "features/readme.md", 46 | "features/step_definitions/doc_steps.rb", 47 | "features/step_definitions/gimme_steps.rb", 48 | "features/support/animals.rb", 49 | "features/support/env.rb", 50 | "features/support/hooks.rb", 51 | "gimme.gemspec", 52 | "lib/gimme-double.rb", 53 | "lib/gimme.rb", 54 | "lib/gimme/captor.rb", 55 | "lib/gimme/compares_args.rb", 56 | "lib/gimme/dsl.rb", 57 | "lib/gimme/ensures_class_method_restoration.rb", 58 | "lib/gimme/errors.rb", 59 | "lib/gimme/finds_stubbings.rb", 60 | "lib/gimme/gives.rb", 61 | "lib/gimme/gives_class_methods.rb", 62 | "lib/gimme/invocation_store.rb", 63 | "lib/gimme/invokes_satisfied_stubbing.rb", 64 | "lib/gimme/matchers.rb", 65 | "lib/gimme/method_store.rb", 66 | "lib/gimme/reset.rb", 67 | "lib/gimme/resolves_methods.rb", 68 | "lib/gimme/rspec_adapter.rb", 69 | "lib/gimme/spies_on_class_methods.rb", 70 | "lib/gimme/store.rb", 71 | "lib/gimme/stubbing_store.rb", 72 | "lib/gimme/test_double.rb", 73 | "lib/gimme/verifies.rb", 74 | "lib/gimme/verifies_class_methods.rb", 75 | "spec/gimme/captor_spec.rb", 76 | "spec/gimme/errors_spec.rb", 77 | "spec/gimme/gives_class_methods_spec.rb", 78 | "spec/gimme/gives_spec.rb", 79 | "spec/gimme/invocation_store_spec.rb", 80 | "spec/gimme/matchers_spec.rb", 81 | "spec/gimme/resolves_methods_spec.rb", 82 | "spec/gimme/rspec_adapter_spec.rb", 83 | "spec/gimme/shared_examples/shared_gives_examples.rb", 84 | "spec/gimme/shared_examples/shared_verifies_examples.rb", 85 | "spec/gimme/spies_on_class_method_spec.rb", 86 | "spec/gimme/test_double_spec.rb", 87 | "spec/gimme/verifies_class_methods_spec.rb", 88 | "spec/gimme/verifies_spec.rb", 89 | "spec/spec_helper.rb" 90 | ] 91 | s.homepage = "http://github.com/searls/gimme" 92 | s.licenses = ["MIT"] 93 | s.require_paths = ["lib"] 94 | s.rubygems_version = "1.8.23" 95 | s.summary = "gimme \u{2014}\u{a0}a low-specification test double library for Ruby" 96 | 97 | if s.respond_to? :specification_version then 98 | s.specification_version = 3 99 | 100 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 101 | s.add_development_dependency(%q, [">= 0"]) 102 | s.add_development_dependency(%q, [">= 0"]) 103 | s.add_development_dependency(%q, [">= 0"]) 104 | s.add_development_dependency(%q, [">= 0"]) 105 | s.add_development_dependency(%q, [">= 0"]) 106 | s.add_development_dependency(%q, [">= 0"]) 107 | s.add_development_dependency(%q, [">= 0"]) 108 | s.add_development_dependency(%q, [">= 0"]) 109 | s.add_development_dependency(%q, [">= 0"]) 110 | s.add_development_dependency(%q, [">= 0"]) 111 | s.add_development_dependency(%q, [">= 0"]) 112 | s.add_development_dependency(%q, [">= 0"]) 113 | s.add_development_dependency(%q, [">= 0"]) 114 | s.add_development_dependency(%q, [">= 1.3.1"]) 115 | s.add_development_dependency(%q, [">= 0.10.0"]) 116 | else 117 | s.add_dependency(%q, [">= 0"]) 118 | s.add_dependency(%q, [">= 0"]) 119 | s.add_dependency(%q, [">= 0"]) 120 | s.add_dependency(%q, [">= 0"]) 121 | s.add_dependency(%q, [">= 0"]) 122 | s.add_dependency(%q, [">= 0"]) 123 | s.add_dependency(%q, [">= 0"]) 124 | s.add_dependency(%q, [">= 0"]) 125 | s.add_dependency(%q, [">= 0"]) 126 | s.add_dependency(%q, [">= 0"]) 127 | s.add_dependency(%q, [">= 0"]) 128 | s.add_dependency(%q, [">= 0"]) 129 | s.add_dependency(%q, [">= 0"]) 130 | s.add_dependency(%q, [">= 1.3.1"]) 131 | s.add_dependency(%q, [">= 0.10.0"]) 132 | end 133 | else 134 | s.add_dependency(%q, [">= 0"]) 135 | s.add_dependency(%q, [">= 0"]) 136 | s.add_dependency(%q, [">= 0"]) 137 | s.add_dependency(%q, [">= 0"]) 138 | s.add_dependency(%q, [">= 0"]) 139 | s.add_dependency(%q, [">= 0"]) 140 | s.add_dependency(%q, [">= 0"]) 141 | s.add_dependency(%q, [">= 0"]) 142 | s.add_dependency(%q, [">= 0"]) 143 | s.add_dependency(%q, [">= 0"]) 144 | s.add_dependency(%q, [">= 0"]) 145 | s.add_dependency(%q, [">= 0"]) 146 | s.add_dependency(%q, [">= 0"]) 147 | s.add_dependency(%q, [">= 1.3.1"]) 148 | s.add_dependency(%q, [">= 0.10.0"]) 149 | end 150 | end 151 | 152 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Gimme 2 | 3 | [![Build Status](https://travis-ci.org/searls/gimme.svg?branch=master)](https://travis-ci.org/searls/gimme) [![Coverage Status](https://coveralls.io/repos/searls/gimme/badge.svg)](https://coveralls.io/r/searls/gimme) 4 | 5 | Gimme is a very lightweight test double library for ruby. Written to be an opinionated (but not noisy) means to facilitate test-driving by enabling the author to specify only what she cares about. If I could only feed Google one thing at this point, it would be: "[Mockito](http://mockito.org/) for Ruby" 6 | 7 | You can read the (possibly stale) documentation below or the (fresh) [gimme Cucumber features on Relish](http://relishapp.com/searls/gimme) 8 | 9 | And here's a [blog post outlining the case for gimme](http://searls.testdouble.com/2011/06/03/whats-wrong-with-rubys-test-doubles/). 10 | 11 | ## Basics (or "What does it Gimme?" ... har.) 12 | 13 | Gimme was originally named (well, for the first five hours of its life) "[Tabula Rasa](http://en.wikipedia.org/wiki/Tabula_rasa)," to very clearly indicate that it generates blank slate test doubles that lack any initial coupling with the concepts associated with specific [test double](http://xunitpatterns.com/Test%20Double.html) subtypes like mocks/stubs/fakes/spies/proxies. But in the end, "gimme" was easier to type than "tabula rasa", and I generally wanted to avoid test pattern lingo from leaking into the context and language of everybody's tests (hence no method named "stub"). 14 | 15 | Gimme doubles are most accurately identified as [test spies](http://xunitpatterns.com/Test%20Spy.html) in [this table discriminating the types](http://xunitpatterns.com/Mocks,%20Fakes,%20Stubs%20and%20Dummies.html) over at Gerard Meszaros' helpful xUnit patterns repository. 16 | 17 | Gimme aims to enable you to write low-friction, low-specification tests that feel a little more like [Mockito](http://mockito.org/) than existing ruby test double libraries. Gimme should do whatever it can to help you isolate your SUT from its dependencies and then get out of your way. And if gimme can provide some fast-feedback about potential problems, it should try to do that too. 18 | 19 | The few things it gives you: 20 | 21 | * Many typical test double library features, like: stubbing & verifying methods, argument matchers for determining what gets stubbed and what to verify, and argument captors for inspecting stuff your [SUT](http://xunitpatterns.com/SUT.html) passes its dependencies 22 | * Natural arrange-act-assert flow — meaning that you can call `verify` after you've interacted with your system under test. 23 | * No stringy/symbolic representations of methods — similar to [rr](https://github.com/btakita/rr), gimme uses the blank slate pattern and `method_missing` to allow for minimally terse stubs and verifications 24 | * Sometimes you know the class of a dependency of your SUT; when you do, gimme can try to help out by raising a NoMethodError when you attempt to stub or verify a method that the class doesn't respond to. 25 | * Gimme won't punish you for not setting up an expectation for every interaction your SUT has with your test double, leaving you to verify exactly what matters to you in the context of what you're building; sometimes specifying the behavior of your SUT on a collaborator is significant, and sometimes it isn't. 26 | 27 | 28 | ## The Disclaimer 29 | 30 | Gimme is still in early development and a little light on features / hardening. While gimme should be enough to get started writing tests/specs, you'll likely run into edge cases that haven't been handled yet. If you're willing to try out gimme on your next toy project and either submit issues or pull requests when you run into issues, hopefully we can work together to make gimme a first-class test double framework in the Ruby community. 31 | 32 | ## Getting started 33 | 34 | ### Setting up 35 | First, install the gem: 36 | 37 | gem install gimme 38 | 39 | Next, wherever you set up your test environment, require gimme: 40 | 41 | require 'gimme' 42 | 43 | And if you're using RSpec, you can get doubled class methods to teardown appropriately by configuring gimme as your mock framework: 44 | 45 | ```ruby 46 | RSpec.configure do |config| 47 | config.mock_framework = Gimme::RSpecAdapter 48 | end 49 | ``` 50 | 51 | If you're introducing gimme to a suite that already uses another double library, you can just as well do this: 52 | 53 | ```ruby 54 | after(:each) do 55 | Gimme.reset 56 | end 57 | ``` 58 | 59 | ### Creating a double 60 | Once you're in your test or spec, to create a test double. 61 | 62 | If you know what what class your SUT will be depending on, you can specify it: 63 | 64 | ```ruby 65 | double = gimme(Object) 66 | ``` 67 | 68 | Or you could just create a generic double can stub/verify any method you need: 69 | 70 | ```ruby 71 | double = gimme() 72 | ``` 73 | 74 | ### Stubbing 75 | 76 | Once you have your double, you can stub methods: 77 | 78 | ```ruby 79 | give(double).to_s { 'Pants' } 80 | double.to_s #=> 'Pants' 81 | 82 | give(double).equal?(:ninja) { true } 83 | give(double).equal?(:fruit) { false } 84 | double.equal?(:ninja) #=> true 85 | ``` 86 | 87 | You can also stub your double to raise an exception (or really, do anything in the passed block): 88 | 89 | ```ruby 90 | dog = gimme(Dog) 91 | give(dog).holler_at(:mail_man) { raise LeashLawError } 92 | 93 | dog.holler_at(:mail_man) # raises LeashLawError 94 | ``` 95 | 96 | ### Verifying 97 | 98 | You can also verify interactions with your double 99 | 100 | ```ruby 101 | double.equal?(:fruit) 102 | 103 | verify(double).equal?(:fruit) # passes verification (read: does nothing) 104 | verify(double).equal?(:what_the) # fails verification (raises a Gimme::VerifyFailedError) 105 | ``` 106 | 107 | You can also specify how many times a specific invocation should have occurred (defaults to 1): 108 | 109 | ```ruby 110 | double.equal?(:fruit) 111 | double.equal?(:fruit) 112 | 113 | verify(double,2).equal?(:fruit) 114 | verify(double,2.times).equal?(:fruit) # N.times syntax needs ruby >= 1.8.7 115 | ``` 116 | 117 | #### Stubbing class methods 118 | 119 | Is to be noted that in the previous examples stubing is done on instance methods. If you need to stub a class method for example ```Dog::habilities```: 120 | 121 | ```ruby 122 | class Dog 123 | self.habilities 124 | # (...) 125 | end 126 | end 127 | ``` 128 | 129 | you can: 130 | 131 | ```ruby 132 | give(Dog).habilities { [:bark, :drool] } 133 | 134 | Dog.habilities #=> [:bark, :drool] 135 | 136 | verify(Dog).habilities #=> passes 137 | ``` 138 | 139 | ### Using Argument Matchers 140 | 141 | Gimme includes several argument matchers which can be used to control which invocations will satisfy a particular stubbing or verification. 142 | 143 | **anything** 144 | 145 | Replacing an argument with `anything` will instantiate a `Gimme::Matchers::Anything` matcher, which always returns true, regardless of what gets passed in. 146 | 147 | ```ruby 148 | give(dog).walk_to(anything,5) { 'Park' } 149 | 150 | walk_to(3,5) #=> 'Park' 151 | walk_to('pants',5) #=> 'Park' 152 | walk_to(nil,5) #=> 'Park' 153 | walk_to(3,5.1) #=> nil 154 | ``` 155 | 156 | Matchers can be used when both stubbing and verifying a method. To verify on anything, you could: 157 | 158 | ```ruby 159 | dog.holler_at(true) 160 | 161 | verify(dog).holler_at(anything) #=> passes verification 162 | ``` 163 | 164 | Other matchers: 165 | 166 | * **is_a(class)** — matches any arguments that are `kind_of?` the provided class 167 | * **any(class)** — same as `is_a`, but also matches nil 168 | * **boolean** — matches true or false arguments 169 | * **numeric** — matches numeric arguments 170 | 171 | See the [cucumber feature for examples using these matchers](https://www.relishapp.com/searls/gimme/docs/old/stubbing-with-matchers) 172 | 173 | #### Writing Custom Argument Matchers 174 | 175 | It's pretty easy to roll your own argument matchers as well. All you really need to do is pass as an argument to a method stubbed by `give` or verified by `verify` an object 176 | that can respond to `matches?(arg)`. Maybe something like this would work (even though it'd be of questionable utility): 177 | 178 | ```ruby 179 | class Nothing 180 | def matches?(arg) 181 | false 182 | end 183 | end 184 | 185 | give(dog).introduce_to(Nothing.new) { :meow } #b/c Nothing.matches? always returns false, :meow will never returned by the double. 186 | ``` 187 | 188 | ### Using Argument Captors 189 | 190 | An instance of an argument `Captor`, when paired with the `capture` matcher, is a valuable way for your test to get at the values that your SUT passes to its collaborators. Often, classes are responsible for building objects to be ingested by their collaborators but for which normal state verification would either be difficult or nonsensical. Argument captors should only be necessary sparingly for most types of applications, but they're a handy tool to have in the toolbox. 191 | 192 | In cases like these, a captor can be used to "capture" the real argument value that the system under test passes its collaborator. This pseudocode provides an example: 193 | 194 | ```ruby 195 | #arrange 196 | searches_system = gimme(SearchesSystem) 197 | sut = QueryExecutor.new(searches_system) 198 | query_captor = Gimme::Captor.new 199 | 200 | #act 201 | sut.submit_query_for_string("find dogs") 202 | 203 | #assert 204 | verify(searches_system).execute(capture(query_captor)) 205 | query_captor.value.table_name.should == "Dogs" 206 | ``` 207 | 208 | ### Suppressing NoMethodError 209 | 210 | You may be reading this section because you got this message: 211 | 212 | The Test Double of may not know how to respond to the '' method. 213 | If you're confident that a real Kernel will know how to respond to '', then you can 214 | invoke give! or verify! to suppress this error. 215 | 216 | Whenever you stub or verify a method against a test double on a class, gimme will first verify that the method can be found on the class being 217 | doubled. Since the vast majority of methods can be verified in this way, this default behavior is designed to provide fast failure. 218 | This can be really handy, whether the cause is as simple as a transcription error of a method name from irb or as convoluted as an incorrect version of a dependency that lacks the method you expected. 219 | 220 | However, because classes can be reopened and edited at runtime, often you'll outsmart gimme by knowing that a particular 221 | method *will* be available on the class being doubled, even though it isn't *right now*. 222 | 223 | For these situations, you could either (1) declare your double without a class argument (e.g. `gimme()` instead of `gimme(Dog)`), or (2) use `give!` and `verify!` to suppress the check that triggers the NoMethodError from being raised. 224 | 225 | Here's an example where our Dog is again being doubled to facilitate some test, and even though the Dog class lacks a public `meow()` method, we happen to know that at runtime, the newest version of the `bananimals` gem will reopen Dog and add `meow()` to it. 226 | 227 | ```ruby 228 | dog = gimme(Dog) 229 | give!(dog).meow { :purr } 230 | 231 | dog.meow #=> :purr 232 | ``` 233 | 234 | We can accomplish the same thing using `verify!`: 235 | 236 | ```ruby 237 | dog = gimme(Dog) 238 | 239 | dog.meow 240 | 241 | verify!(dog).meow #=> verification passes, even though gimme can't see the meow method. 242 | ``` 243 | 244 | ### gimme_next 245 | 246 | To my knowledge, there isn't an established pattern or name for this next feature. Sometimes you may want your SUT to instantiate its own dependency. However, if you also want to achieve isolation from the implementation details of that dependency, you can use `gimme_next`. 247 | 248 | Take this example method from the RSpec book: 249 | 250 | ```ruby 251 | def guess(guess) 252 | marker = Marker.new(@secret,guess) 253 | @output.puts '+'*marker.exact_match_count + '-'*marker.number_match_count 254 | end 255 | ``` 256 | 257 | This can be tested with gimme in isolation (meaning that a real Marker object is never instantiated or invoked) like so: 258 | 259 | ```ruby 260 | describe '#guess' do 261 | let(:marker) { gimme_next(Marker) } 262 | before do 263 | give(marker).exact_match_count { 4 } 264 | give(marker).number_match_count { 0 } 265 | 266 | game.guess('1234') 267 | end 268 | 269 | it 'instantiates a marker with the secret and guess' do 270 | verify!(marker).initialize('1234','1234') 271 | end 272 | 273 | it 'outputs the exact matches followed by the number matches' do 274 | verify(output).puts('++++') 275 | end 276 | end 277 | ``` 278 | 279 | As you can see above, `gimme_next(Marker)` will create a double just like `gimme()` would have, but it will also temporarily redefine the passed class's `new` method such that the next instantiation of that class (presumably by the SUT) will return the same double.* 280 | 281 | This way we can clearly specify the SUT's interaction with the Marker class while maintaining its isolation. 282 | 283 | *Subsequent instantiations of the passed class will continue to return normal instances. 284 | 285 | ## About 286 | 287 | ### Maintainers 288 | * [Justin Searls](http://about.me/searls), [test double](http://testdouble.com) 289 | --------------------------------------------------------------------------------