├── .gitignore ├── .travis.yml ├── Appraisals ├── CONTRIBUTING.md ├── Gemfile ├── MIT-LICENSE ├── NEWS.md ├── README.md ├── Rakefile ├── ext └── mkrf_conf.rb ├── gemfiles ├── rails_3.1.gemfile ├── rails_3.2.gemfile ├── rails_4.0.gemfile ├── rails_4.1.gemfile └── rails_4.2.gemfile ├── lib ├── shoulda-callback-matchers.rb └── shoulda │ └── callback │ ├── matchers.rb │ └── matchers │ ├── active_model.rb │ ├── integrations │ ├── rspec.rb │ └── test_unit.rb │ ├── rails_version_helper.rb │ └── version.rb ├── shoulda-callback-matchers.gemspec └── spec ├── shoulda └── active_model │ └── callback_matcher_spec.rb ├── spec_helper.rb └── support ├── class_builder.rb └── model_builder.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | .rspec 3 | .ruby-gemset 4 | .ruby-version 5 | test/*/log/*.log 6 | doc 7 | coverage 8 | .svn/ 9 | pkg 10 | *.swp 11 | *.swo 12 | tags 13 | tmp 14 | .bundle 15 | .git 16 | .zedstate 17 | *.rbc 18 | *.gem 19 | gemfiles/.bundle/* 20 | gemfiles/*.lock 21 | Gemfile.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 2.0.0 4 | - 2.1.10 5 | - 2.2.5 6 | - 2.3.1 7 | - rbx-2 8 | - jruby-19mode 9 | - jruby-20mode 10 | - jruby-head 11 | 12 | 13 | sudo: false 14 | before_install: gem install bundler 15 | script: "bundle exec rspec" 16 | 17 | 18 | gemfile: 19 | - gemfiles/rails_4.2.gemfile 20 | - gemfiles/rails_4.1.gemfile 21 | - gemfiles/rails_4.0.gemfile 22 | - gemfiles/rails_3.2.gemfile 23 | - gemfiles/rails_3.1.gemfile 24 | 25 | 26 | env: 27 | matrix: 28 | - JRUBY_OPTS=--2.0 29 | - JRUBY_OPTS= 30 | 31 | 32 | matrix: 33 | exclude: 34 | - rvm: jruby-head 35 | gemfile: gemfiles/rails_4.2.gemfile 36 | - rvm: jruby-head 37 | gemfile: gemfiles/rails_3.1.gemfile 38 | - rvm: 2.2.5 39 | gemfile: gemfiles/rails_4.0.gemfile 40 | - rvm: 2.2.5 41 | gemfile: gemfiles/rails_3.2.gemfile 42 | - rvm: 2.2.5 43 | gemfile: gemfiles/rails_3.1.gemfile 44 | - rvm: 2.3.1 45 | gemfile: gemfiles/rails_4.0.gemfile 46 | - rvm: 2.3.1 47 | gemfile: gemfiles/rails_3.2.gemfile 48 | - rvm: 2.3.1 49 | gemfile: gemfiles/rails_3.1.gemfile 50 | - rvm: 1.9.3 51 | gemfile: gemfiles/rails_4.2.gemfile 52 | - rvm: 1.9.3 53 | gemfile: gemfiles/rails_4.1.gemfile 54 | - rvm: 1.9.3 55 | gemfile: gemfiles/rails_4.0.gemfile 56 | - rvm: jruby-19mode 57 | gemfile: gemfiles/rails_4.2.gemfile 58 | - rvm: jruby-19mode 59 | gemfile: gemfiles/rails_4.1.gemfile 60 | - rvm: jruby-19mode 61 | gemfile: gemfiles/rails_4.0.gemfile 62 | - rvm: jruby-19mode 63 | env: JRUBY_OPTS=--2.0 64 | - rvm: jruby-20mode 65 | env: JRUBY_OPTS= 66 | - rvm: jruby-head 67 | env: JRUBY_OPTS=--2.0 68 | - rvm: 1.9.3 69 | env: JRUBY_OPTS=--2.0 70 | - rvm: 2.0.0 71 | env: JRUBY_OPTS=--2.0 72 | - rvm: 2.1.10 73 | env: JRUBY_OPTS=--2.0 74 | - rvm: 2.2.5 75 | env: JRUBY_OPTS=--2.0 76 | - rvm: 2.3.1 77 | env: JRUBY_OPTS=--2.0 78 | - rvm: rbx-2 79 | env: JRUBY_OPTS=--2.0 80 | allow_failures: 81 | - rvm: jruby-20mode 82 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise 'rails 4.2' do 2 | gem 'rails', '~> 4.2.0' 3 | gem 'jquery-rails' 4 | gem 'sass-rails', '~> 5.0' 5 | gem 'mocha' 6 | end 7 | 8 | appraise 'rails 4.1' do 9 | gem 'rails', '~> 4.1.0' 10 | gem 'jquery-rails' 11 | gem 'sass-rails', '~> 5.0' 12 | gem 'mocha' 13 | end 14 | 15 | appraise 'rails 4.0' do 16 | gem 'rails', '~> 4.0.0' 17 | gem 'jquery-rails' 18 | gem 'sass-rails' 19 | end 20 | 21 | appraise 'rails 3.2' do 22 | gem 'rails', '~> 3.2.0' 23 | gem 'jquery-rails' 24 | gem 'sass-rails' 25 | gem 'test-unit', '~> 3.0' 26 | end 27 | 28 | appraise 'rails 3.1' do 29 | gem 'rails', '~> 3.1.0' 30 | gem 'jquery-rails' 31 | gem 'sass-rails' 32 | end 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdliss/shoulda-callback-matchers/b8a3680bc1d19ac713e43054a2b33238ce845698/CONTRIBUTING.md -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | 5 | # For test Rails application 6 | gem 'sqlite3', platform: :ruby 7 | 8 | # Can't wrap in platform :jruby do...end block because appraisal doesn't support 9 | # it 10 | gem 'activerecord-jdbc-adapter', platform: :jruby 11 | gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby 12 | gem 'jdbc-sqlite3', platform: :jruby 13 | gem 'jruby-openssl', platform: :jruby 14 | gem 'therubyrhino', platform: :jruby 15 | 16 | gem 'psych', platform: :rbx 17 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Beat Richartz 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # v0.1.0 2 | * initial commit -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Shoulda Callback Matchers 2 | [![Gem Version](https://badge.fury.io/rb/shoulda-callback-matchers.svg)](http://badge.fury.io/rb/shoulda-callback-matchers)[![Build Status](https://travis-ci.org/jdliss/shoulda-callback-matchers.svg?branch=master)](https://travis-ci.org/jdliss/shoulda-callback-matchers) [![Code Climate](https://codeclimate.com/github/beatrichartz/shoulda-callback-matchers.png)](https://codeclimate.com/github/beatrichartz/shoulda-callback-matchers) [![Dependency Status](https://gemnasium.com/beatrichartz/shoulda-callback-matchers.svg)](https://gemnasium.com/beatrichartz/shoulda-callback-matchers) 3 | 4 | Matchers to test before, after and around hooks(currently supports method and object callbacks): 5 | 6 | ## Usage 7 | 8 | Method Callbacks: 9 | 10 | ````ruby 11 | describe Post do 12 | it { is_expected.to callback(:count_comments).before(:save) } 13 | it { is_expected.to callback(:post_to_twitter).after(:create) } 14 | it { is_expected.to callback(:evaluate_if_is_should_validate).before(:validation) } 15 | it { is_expected.to callback(:add_some_convenience_accessors).after(:find) } 16 | 17 | # with conditions 18 | 19 | it { is_expected.to callback(:assign_something).before(:create).if(:this_is_true) } 20 | it { is_expected.to callback(:destroy_something_else).before(:destroy).unless(:this_is_true) } 21 | end 22 | 23 | describe User do 24 | it { is_expected.not_to callback(:make_email_validation_ready!).before(:validation).on(:update) } 25 | it { is_expected.to callback(:make_email_validation_ready!).before(:validation).on(:create) } 26 | it { is_expected.to callback(:update_user_count).before(:destroy) } 27 | end 28 | ```` 29 | 30 | Object Callbacks: 31 | 32 | ````ruby 33 | class CallbackClass 34 | def before_save 35 | ... 36 | end 37 | 38 | def after_create 39 | ... 40 | end 41 | 42 | def before_validation 43 | ... 44 | end 45 | 46 | def after_find 47 | ... 48 | end 49 | end 50 | 51 | describe Post do 52 | it { is_expected.to callback(CallbackClass).before(:save) } 53 | it { is_expected.to callback(CallbackClass).after(:create) } 54 | it { is_expected.to callback(CallbackClass).before(:validation) } 55 | it { is_expected.to callback(CallbackClass).after(:find) } 56 | 57 | # with conditions 58 | it { is_expected.to callback(CallbackClass).before(:create).if(:this_is_true) } 59 | it { is_expected.to callback(CallbackClass).after(:find).unless(:is_this_true?) } 60 | end 61 | 62 | describe User do 63 | it { is_expected.not_to callback(CallbackClass).before(:validation).on(:update) } 64 | it { is_expected.to callback(CallbackClass).before(:validation).on(:create) } 65 | # Only Rails > 3.2+ 66 | it { is_expected.to callback(CallbackClass).before(:validation).on([:create, :update]) } 67 | it { is_expected.to callback(CallbackClass).before(:destroy) } 68 | end 69 | ```` 70 | 71 | This will test: 72 | - the method call 73 | - method existence 74 | 75 | Either on the model itself or on the callback object. Be aware that obviously this does not test the callback method or object itself. It makes testing via triggering the callback events (validation, save) unnecessary, but you still have to test the called procedure seperately. 76 | 77 | In Rails 3 or 4 and Bundler, add the following to your Gemfile: 78 | 79 | ````ruby 80 | group :test do 81 | gem 'shoulda-callback-matchers', '~> 1.1.1' 82 | end 83 | ```` 84 | 85 | This gem uses semantic versioning, so you won't have incompability issues with patches. 86 | 87 | rspec-rails needs to be in the development group so that Rails generators work. 88 | 89 | ````ruby 90 | group :development, :test do 91 | gem "rspec-rails" 92 | end 93 | ```` 94 | 95 | Shoulda will automatically include matchers into the appropriate example groups. 96 | 97 | ## Troubleshooting 98 | 99 | ### RSpec + Spring 100 | #### undefined method `callback' 101 | 102 | If you're getting this error, it's probably due to classes being redefined by Spring - currently this library does not accommodate for reloaded classes. The easiest fix is to load the matchers into the test library config in your `rails_helper.rb`: 103 | 104 | ```ruby 105 | RSpec.configure do |config| 106 | config.include(Shoulda::Callback::Matchers::ActiveModel) 107 | end 108 | ``` 109 | 110 | ## Credits 111 | 112 | This gem is maintained by me and its contributors, 113 | Shoulda is maintained and funded by [thoughtbot](http://thoughtbot.com/community) 114 | 115 | ## Contributors & Contributions 116 | - @pvertenten (callback objects) 117 | - @johnnyshields (bugfixes) 118 | - @esbarango (README updates) 119 | - @yuku-t (Rails 4.2 Support) 120 | 121 | Let's make this gem useful, send me a PR if you've discovered an issue you'd like to fix! 122 | 123 | ## License 124 | 125 | Shoulda is Copyright © 2006-2014 thoughtbot, inc. 126 | Callback Matchers is Copyright © 2014 Beat Richartz 127 | It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file. 128 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'bundler/gem_tasks' 3 | require 'rspec/core/rake_task' 4 | require 'cucumber/rake/task' 5 | require 'appraisal' 6 | 7 | RSpec::Core::RakeTask.new do |t| 8 | t.pattern = "spec/**/*_spec.rb" 9 | t.rspec_opts = '--color --format progress' 10 | t.verbose = false 11 | end 12 | 13 | desc 'Test the plugin' 14 | task :all => ["appraisal:cleanup", "appraisal:install"] do 15 | exec('rake appraisal spec') 16 | end 17 | 18 | desc 'Default: run specs' 19 | task :default => [:all] 20 | -------------------------------------------------------------------------------- /ext/mkrf_conf.rb: -------------------------------------------------------------------------------- 1 | rbx = defined?(RUBY_ENGINE) && 'rbx' == RUBY_ENGINE 2 | 3 | def already_installed(dep) 4 | !Gem::DependencyInstaller.new(domain: :local).find_gems_with_sources(dep).empty? || 5 | !Gem::DependencyInstaller.new(domain: :local, prerelease: true).find_gems_with_sources(dep).empty? 6 | end 7 | 8 | if rbx 9 | require 'rubygems' 10 | require 'rubygems/command.rb' 11 | require 'rubygems/dependency.rb' 12 | require 'rubygems/dependency_installer.rb' 13 | 14 | begin 15 | Gem::Command.build_args = ARGV 16 | rescue NoMethodError 17 | end 18 | 19 | dep = [ 20 | Gem::Dependency.new("rubysl", '~> 2.0'), 21 | Gem::Dependency.new("rubysl-test-unit", '~> 2.0'), 22 | Gem::Dependency.new("racc", '~> 1.4') 23 | ].reject{|d| already_installed(d) } 24 | 25 | begin 26 | puts "Installing base gem" 27 | inst = Gem::DependencyInstaller.new 28 | dep.each {|d| inst.install d } 29 | rescue 30 | inst = Gem::DependencyInstaller.new(prerelease: true) 31 | begin 32 | dep.each {|d| inst.install d } 33 | rescue Exception => e 34 | puts e 35 | puts e.backtrace.join "\n " 36 | exit(1) 37 | end 38 | end unless dep.size == 0 39 | end 40 | 41 | # create dummy rakefile to indicate success 42 | f = File.open(File.join(File.dirname(__FILE__), "Rakefile"), "w") 43 | f.write("task :default\n") 44 | f.close -------------------------------------------------------------------------------- /gemfiles/rails_3.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "sqlite3", :platform=>:ruby 6 | gem "activerecord-jdbc-adapter", :platform=>:jruby 7 | gem "activerecord-jdbcsqlite3-adapter", :platform=>:jruby 8 | gem "jdbc-sqlite3", :platform=>:jruby 9 | gem "jruby-openssl", :platform=>:jruby 10 | gem "therubyrhino", :platform=>:jruby 11 | gem "psych", :platform=>:rbx 12 | gem "rails", "~> 3.1.0" 13 | gem "jquery-rails" 14 | gem "sass-rails" 15 | 16 | gemspec :path=>"../" -------------------------------------------------------------------------------- /gemfiles/rails_3.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "sqlite3", :platform=>:ruby 6 | gem "activerecord-jdbc-adapter", :platform=>:jruby 7 | gem "activerecord-jdbcsqlite3-adapter", :platform=>:jruby 8 | gem "jdbc-sqlite3", :platform=>:jruby 9 | gem "jruby-openssl", :platform=>:jruby 10 | gem "therubyrhino", :platform=>:jruby 11 | gem "psych", :platform=>:rbx 12 | gem "rails", "~> 3.2.0" 13 | gem "jquery-rails" 14 | gem "sass-rails" 15 | gem "test-unit", "~> 3.0" 16 | 17 | gemspec :path=>"../" -------------------------------------------------------------------------------- /gemfiles/rails_4.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "sqlite3", :platform=>:ruby 6 | gem "activerecord-jdbc-adapter", :platform=>:jruby 7 | gem "activerecord-jdbcsqlite3-adapter", :platform=>:jruby 8 | gem "jdbc-sqlite3", :platform=>:jruby 9 | gem "jruby-openssl", :platform=>:jruby 10 | gem "therubyrhino", :platform=>:jruby 11 | gem "psych", :platform=>:rbx 12 | gem "rails", "~> 4.0.0" 13 | gem "jquery-rails" 14 | gem "sass-rails" 15 | 16 | gemspec :path=>"../" -------------------------------------------------------------------------------- /gemfiles/rails_4.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "sqlite3", :platform => :ruby 6 | gem "activerecord-jdbc-adapter", :platform => :jruby 7 | gem "activerecord-jdbcsqlite3-adapter", :platform => :jruby 8 | gem "jdbc-sqlite3", :platform => :jruby 9 | gem "jruby-openssl", :platform => :jruby 10 | gem "therubyrhino", :platform => :jruby 11 | gem "psych", :platform => :rbx 12 | gem "rails", "~> 4.1.0" 13 | gem "jquery-rails" 14 | gem "sass-rails", "~> 5.0" 15 | gem "mocha" 16 | 17 | gemspec :path => "../" 18 | -------------------------------------------------------------------------------- /gemfiles/rails_4.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "sqlite3", :platform => :ruby 6 | gem "activerecord-jdbc-adapter", :platform => :jruby 7 | gem "activerecord-jdbcsqlite3-adapter", :platform => :jruby 8 | gem "jdbc-sqlite3", :platform => :jruby 9 | gem "jruby-openssl", :platform => :jruby 10 | gem "therubyrhino", :platform => :jruby 11 | gem "psych", :platform => :rbx 12 | gem "rails", "~> 4.2.0" 13 | gem "jquery-rails" 14 | gem "sass-rails", "~> 5.0" 15 | gem "mocha" 16 | 17 | gemspec :path => "../" 18 | -------------------------------------------------------------------------------- /lib/shoulda-callback-matchers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'shoulda/callback/matchers' 2 | -------------------------------------------------------------------------------- /lib/shoulda/callback/matchers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'matchers/version' 2 | require_relative 'matchers/rails_version_helper' 3 | 4 | if defined?(RSpec) 5 | require_relative 'matchers/integrations/rspec' 6 | end 7 | 8 | require_relative 'matchers/integrations/test_unit' 9 | -------------------------------------------------------------------------------- /lib/shoulda/callback/matchers/active_model.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module Shoulda # :nodoc: 4 | module Callback # :nodoc: 5 | module Matchers # :nodoc: 6 | module ActiveModel # :nodoc: 7 | 8 | # Ensures that the given model has a callback defined for the given method 9 | # 10 | # Options: 11 | # * before(:lifecycle). Symbol. - define the callback as a callback before the fact. :lifecycle can be :save, :create, :update, :destroy, :validation 12 | # * after(:lifecycle). Symbol. - define the callback as a callback after the fact. :lifecycle can be :save, :create, :update, :destroy, :validation, :initialize, :find, :touch 13 | # * around(:lifecycle). Symbol. - define the callback as a callback around the fact. :lifecycle can be :save, :create, :update, :destroy 14 | # if(:condition). Symbol. - add a positive condition to the callback to be matched against 15 | # unless(:condition). Symbol. - add a negative condition to the callback to be matched against 16 | # 17 | # Examples: 18 | # it { should callback(:method).after(:create) } 19 | # it { should callback(:method).before(:validation).unless(:should_it_not?) } 20 | # it { should callback(CallbackClass).before(:validation).unless(:should_it_not?) } 21 | # 22 | def callback method 23 | CallbackMatcher.new method 24 | end 25 | 26 | class CallbackMatcher # :nodoc: 27 | VALID_OPTIONAL_LIFECYCLES = [:validation, :commit, :rollback].freeze 28 | 29 | include RailsVersionHelper 30 | 31 | def initialize method 32 | @method = method 33 | end 34 | 35 | # @todo replace with %i() as soon as 1.9 is deprecated 36 | [:before, :after, :around].each do |hook| 37 | define_method hook do |lifecycle| 38 | @hook = hook 39 | @lifecycle = lifecycle 40 | check_for_undefined_callbacks! 41 | 42 | self 43 | end 44 | end 45 | 46 | [:if, :unless].each do |condition_type| 47 | define_method condition_type do |condition| 48 | @condition_type = condition_type 49 | @condition = condition 50 | 51 | self 52 | end 53 | end 54 | 55 | def on optional_lifecycle 56 | check_for_valid_optional_lifecycles! 57 | 58 | @optional_lifecycle = optional_lifecycle 59 | 60 | self 61 | end 62 | 63 | def matches? subject 64 | check_preconditions! 65 | 66 | callbacks = subject.send :"_#{@lifecycle}_callbacks" 67 | callbacks.any? do |callback| 68 | has_callback?(subject, callback) && 69 | matches_hook?(callback) && 70 | matches_conditions?(callback) && 71 | matches_optional_lifecycle?(callback) && 72 | callback_method_exists?(subject, callback) 73 | end 74 | end 75 | 76 | def callback_method_exists? object, callback 77 | if is_class_callback?(object, callback) && !callback_object(object, callback).respond_to?(:"#{@hook}_#{@lifecycle}", true) 78 | @failure_message = "callback #{@method} is listed as a callback #{@hook} #{@lifecycle}#{optional_lifecycle_phrase}#{condition_phrase}, but the given object does not respond to #{@hook}_#{@lifecycle} (using respond_to?(:#{@hook}_#{@lifecycle}, true)" 79 | false 80 | elsif !is_class_callback?(object, callback) && !object.respond_to?(callback.filter, true) 81 | @failure_message = "callback #{@method} is listed as a callback #{@hook} #{@lifecycle}#{optional_lifecycle_phrase}#{condition_phrase}, but the model does not respond to #{@method} (using respond_to?(:#{@method}, true)" 82 | false 83 | else 84 | true 85 | end 86 | end 87 | 88 | def failure_message 89 | @failure_message || "expected #{@method} to be listed as a callback #{@hook} #{@lifecycle}#{optional_lifecycle_phrase}#{condition_phrase}, but was not" 90 | end 91 | 92 | def failure_message_when_negated 93 | @failure_message || "expected #{@method} not to be listed as a callback #{@hook} #{@lifecycle}#{optional_lifecycle_phrase}#{condition_phrase}, but was" 94 | end 95 | 96 | def negative_failure_message 97 | failure_message_when_negated 98 | end 99 | 100 | def description 101 | "callback #{@method} #{@hook} #{@lifecycle}#{optional_lifecycle_phrase}#{condition_phrase}" 102 | end 103 | 104 | private 105 | 106 | def check_preconditions! 107 | check_lifecycle_present! 108 | end 109 | 110 | def check_lifecycle_present! 111 | unless @lifecycle 112 | raise UsageError, "callback #{@method} can not be tested against an undefined lifecycle, use .before, .after or .around", caller 113 | end 114 | end 115 | 116 | def check_for_undefined_callbacks! 117 | if [:rollback, :commit].include?(@lifecycle) && @hook != :after 118 | raise UsageError, "Can not callback before or around #{@lifecycle}, use after.", caller 119 | end 120 | end 121 | 122 | def check_for_valid_optional_lifecycles! 123 | unless VALID_OPTIONAL_LIFECYCLES.include?(@lifecycle) 124 | raise UsageError, "The .on option is only valid for #{VALID_OPTIONAL_LIFECYCLES.to_sentence} and cannot be used with #{@lifecycle}, use with .before(:validation) or .after(:validation)", caller 125 | end 126 | end 127 | 128 | def precondition_failed? 129 | @failure_message.present? 130 | end 131 | 132 | def matches_hook? callback 133 | callback.kind == @hook 134 | end 135 | 136 | def has_callback? subject, callback 137 | has_callback_object?(subject, callback) || has_callback_method?(callback) || has_callback_class?(callback) 138 | end 139 | 140 | def has_callback_method? callback 141 | callback.filter == @method 142 | end 143 | 144 | def has_callback_class? callback 145 | class_callback_required? && callback.filter.is_a?(@method) 146 | end 147 | 148 | def has_callback_object? subject, callback 149 | callback.filter.respond_to?(:match) && 150 | callback.filter.match(/\A_callback/) && 151 | subject.respond_to?(:"#{callback.filter}_object") && 152 | callback_object(subject, callback).class == @method 153 | end 154 | 155 | def matches_conditions? callback 156 | if rails_version >= '4.1' 157 | !@condition || callback.instance_variable_get(:"@#{@condition_type}").include?(@condition) 158 | else 159 | !@condition || callback.options[@condition_type].include?(@condition) 160 | end 161 | end 162 | 163 | def matches_optional_lifecycle? callback 164 | if rails_version >= '4.1' 165 | if_conditions = callback.instance_variable_get(:@if) 166 | !@optional_lifecycle || if_conditions.include?(lifecycle_context_string) || active_model_proc_matches_optional_lifecycle?(if_conditions) 167 | else 168 | !@optional_lifecycle || callback.options[:if].include?(lifecycle_context_string) 169 | end 170 | end 171 | 172 | def condition_phrase 173 | " #{@condition_type} #{@condition} evaluates to #{@condition_type == :if ? 'true' : 'false'}" if @condition 174 | end 175 | 176 | def optional_lifecycle_phrase 177 | " on #{@optional_lifecycle}" if @optional_lifecycle 178 | end 179 | 180 | def lifecycle_context_string 181 | if rails_version >= '4.0' 182 | rails_4_lifecycle_context_string 183 | else 184 | rails_3_lifecycle_context_string 185 | end 186 | end 187 | 188 | def rails_3_lifecycle_context_string 189 | if @lifecycle == :validation 190 | "self.validation_context == :#{@optional_lifecycle}" 191 | else 192 | "transaction_include_action?(:#{@optional_lifecycle})" 193 | end 194 | end 195 | 196 | def rails_4_lifecycle_context_string 197 | if @lifecycle == :validation 198 | "[:#{@optional_lifecycle}].include? self.validation_context" 199 | elsif @optional_lifecycle.kind_of?(Array) 200 | "transaction_include_any_action?(#{@optional_lifecycle})" 201 | else 202 | "transaction_include_any_action?([:#{@optional_lifecycle}])" 203 | end 204 | end 205 | 206 | def active_model_proc_matches_optional_lifecycle? if_conditions 207 | if_conditions.select{|i| i.is_a? Proc }.any? do |condition| 208 | condition.call ValidationContext.new(@optional_lifecycle) 209 | end 210 | end 211 | 212 | def class_callback_required? 213 | !@method.is_a?(Symbol) && !@method.is_a?(String) 214 | end 215 | 216 | def is_class_callback? subject, callback 217 | !callback_object(subject, callback).is_a?(Symbol) && !callback_object(subject, callback).is_a?(String) 218 | end 219 | 220 | def callback_object subject, callback 221 | if (rails_version >= '3.0' && rails_version < '4.1') && !callback.filter.is_a?(Symbol) 222 | subject.send("#{callback.filter}_object") 223 | else 224 | callback.filter 225 | end 226 | end 227 | 228 | end 229 | 230 | ValidationContext = Struct.new :validation_context 231 | UsageError = Class.new NameError 232 | end 233 | end 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /lib/shoulda/callback/matchers/integrations/rspec.rb: -------------------------------------------------------------------------------- 1 | # :enddoc: 2 | 3 | if defined?(::ActiveRecord) 4 | require 'shoulda/callback/matchers/active_model' 5 | module RSpec::Matchers 6 | include Shoulda::Callback::Matchers::ActiveModel 7 | end 8 | elsif defined?(::ActiveModel) 9 | require 'shoulda/callback/matchers/active_model' 10 | module RSpec::Matchers 11 | include Shoulda::Callback::Matchers::ActiveModel 12 | end 13 | end -------------------------------------------------------------------------------- /lib/shoulda/callback/matchers/integrations/test_unit.rb: -------------------------------------------------------------------------------- 1 | # :enddoc: 2 | 3 | include Shoulda::Callback::Matchers::RailsVersionHelper 4 | 5 | # in environments where test/unit is not required, this is necessary 6 | unless defined?(Test::Unit::TestCase) 7 | begin 8 | require rails_version >= '4.1' ? 'minitest' : 'test/unit/testcase' 9 | rescue LoadError 10 | # silent 11 | end 12 | end 13 | 14 | if defined?(Test::Unit::TestCase) && (defined?(::ActiveModel) || defined?(::ActiveRecord)) 15 | require 'shoulda/callback/matchers/active_model' 16 | 17 | Test::Unit::TestCase.tap do |test_unit| 18 | test_unit.send :include, Shoulda::Callback::Matchers::ActiveModel 19 | test_unit.send :extend, Shoulda::Callback::Matchers::ActiveModel 20 | end 21 | 22 | elsif defined?(MiniTest::Unit::TestCase) && (defined?(::ActiveModel) || defined?(::ActiveRecord)) 23 | require 'shoulda/callback/matchers/active_model' 24 | 25 | MiniTest::Unit::TestCase.tap do |minitest_unit| 26 | minitest_unit.send :include, Shoulda::Callback::Matchers::ActiveModel 27 | minitest_unit.send :extend, Shoulda::Callback::Matchers::ActiveModel 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lib/shoulda/callback/matchers/rails_version_helper.rb: -------------------------------------------------------------------------------- 1 | # :enddoc: 2 | module Shoulda 3 | module Callback 4 | module Matchers 5 | module RailsVersionHelper 6 | class RailsVersion 7 | %w(< <= > >= ==).each do |operand| 8 | define_method operand do |version_string| 9 | version_int = convert_str_to_int(version_string) 10 | rails_version_int.send(operand, version_int) 11 | end 12 | end 13 | 14 | private 15 | 16 | def rails_version_int 17 | calculate_version_int(rails_major_version, rails_minor_version) 18 | end 19 | 20 | def convert_str_to_int(version_string) 21 | major, minor = version_string.split('.').map(&:to_i) 22 | calculate_version_int(major, minor) 23 | end 24 | 25 | def calculate_version_int(major, minor) 26 | major * 100 + minor 27 | end 28 | 29 | def rails_major_version 30 | version_module::MAJOR 31 | end 32 | 33 | def rails_minor_version 34 | version_module::MINOR 35 | end 36 | 37 | def version_module 38 | (defined?(::ActiveRecord) ? ::ActiveRecord : ::ActiveModel)::VERSION 39 | end 40 | end 41 | 42 | def rails_version 43 | @rails_version ||= RailsVersion.new 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/shoulda/callback/matchers/version.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Callback 3 | module Matchers 4 | VERSION = '1.1.4'.freeze 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /shoulda-callback-matchers.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.join(File.dirname(__FILE__), 'lib') 2 | require 'shoulda/callback/matchers/version' 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "shoulda-callback-matchers" 6 | s.version = Shoulda::Callback::Matchers::VERSION.dup 7 | s.authors = ["Beat Richartz", "Jonathan Liss"] 8 | s.date = Time.now.strftime("%Y-%m-%d") 9 | s.licenses = ["MIT"] 10 | s.email = "jonacom@lissismore.com" 11 | s.homepage = "http://github.com/jdliss/shoulda-callback-matchers" 12 | s.summary = "Making callback tests easy on the fingers and eyes" 13 | s.description = "Making callback tests easy on the fingers and eyes" 14 | 15 | s.files = `git ls-files`.split("\n") 16 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 17 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 18 | s.require_paths = ["lib"] 19 | s.extensions = 'ext/mkrf_conf.rb' 20 | 21 | s.add_dependency('activesupport', '>= 3') 22 | 23 | s.add_development_dependency('appraisal', '~> 2.1.0') 24 | s.add_development_dependency('aruba') 25 | s.add_development_dependency('bundler', '>= 1.1') 26 | s.add_development_dependency('rails', '>= 3') 27 | s.add_development_dependency('rake', '~> 10') 28 | s.add_development_dependency('rspec-rails', '~> 3') 29 | 30 | if RUBY_ENGINE == 'rbx' 31 | s.add_development_dependency "rubysl", "~> 2" 32 | s.add_development_dependency "rubysl-test-unit", '~> 2' 33 | s.add_development_dependency "racc", "~> 1.4" 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/shoulda/active_model/callback_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Shoulda::Callback::Matchers::ActiveModel do 4 | 5 | context "invalid use" do 6 | before do 7 | @callback_object_class = define_model :callback do 8 | define_method("before_create"){} 9 | define_method("after_save"){} 10 | end 11 | callback_object = @callback_object_class.new 12 | @model = define_model(:example, :attr => :string, 13 | :other => :integer) do 14 | before_create :dance!, :if => :evaluates_to_false! 15 | after_save :shake!, :unless => :evaluates_to_true! 16 | after_create :wiggle! 17 | before_create callback_object, :if => :evaluates_to_false! 18 | after_save callback_object, :unless => :evaluates_to_true! 19 | after_create callback_object 20 | define_method(:shake!){} 21 | define_method(:dance!){} 22 | end.new 23 | 24 | end 25 | it "should return a meaningful error when used without a defined lifecycle" do 26 | expect { callback(:dance!).matches? :foo }.to raise_error Shoulda::Callback::Matchers::ActiveModel::UsageError, 27 | "callback dance! can not be tested against an undefined lifecycle, use .before, .after or .around" 28 | end 29 | it "should return a meaningful error when used with an optional lifecycle without the original lifecycle being validation" do 30 | expect { callback(:dance!).after(:create).on(:save) }.to raise_error Shoulda::Callback::Matchers::ActiveModel::UsageError, 31 | "The .on option is only valid for validation, commit, and rollback and cannot be used with create, use with .before(:validation) or .after(:validation)" 32 | end 33 | it "should return a meaningful error when used without a defined lifecycle" do 34 | expect { callback(@callback_object_class).matches? :foo }.to raise_error Shoulda::Callback::Matchers::ActiveModel::UsageError, 35 | "callback Callback can not be tested against an undefined lifecycle, use .before, .after or .around" 36 | end 37 | it "should return a meaningful error when used with an optional lifecycle without the original lifecycle being validation" do 38 | expect { callback(@callback_object_class).after(:create).on(:save) }.to raise_error Shoulda::Callback::Matchers::ActiveModel::UsageError, 39 | "The .on option is only valid for validation, commit, and rollback and cannot be used with create, use with .before(:validation) or .after(:validation)" 40 | end 41 | it "should return a meaningful error when used with rollback or commit and before" do 42 | expect { callback(@callback_object_class).before(:commit).on(:destroy) }.to raise_error Shoulda::Callback::Matchers::ActiveModel::UsageError, 43 | "Can not callback before or around commit, use after." 44 | end 45 | end 46 | 47 | [:save, :create, :update, :destroy].each do |lifecycle| 48 | context "on #{lifecycle}" do 49 | before do 50 | @callback_object_class = define_model(:callback) do 51 | define_method("before_#{lifecycle}"){} 52 | define_method("after_#{lifecycle}"){} 53 | define_method("around_#{lifecycle}"){} 54 | end 55 | 56 | callback_object = @callback_object_class.new 57 | 58 | @other_callback_object_class = define_model(:other_callback) do 59 | define_method("after_#{lifecycle}"){} 60 | define_method("around_#{lifecycle}"){} 61 | end 62 | 63 | other_callback_object = @other_callback_object_class.new 64 | 65 | @callback_object_not_found_class = define_model(:callback_not_found) do 66 | define_method("before_#{lifecycle}"){} 67 | define_method("after_#{lifecycle}"){} 68 | define_method("around_#{lifecycle}"){} 69 | end 70 | 71 | @model = define_model(:example, :attr => :string, 72 | :other => :integer) do 73 | send(:"before_#{lifecycle}", :dance!, :if => :evaluates_to_false!) 74 | send(:"after_#{lifecycle}", :shake!, :unless => :evaluates_to_true!) 75 | send(:"around_#{lifecycle}", :giggle!) 76 | send(:"before_#{lifecycle}", :wiggle!) 77 | 78 | send(:"before_#{lifecycle}", callback_object, :if => :evaluates_to_false!) 79 | send(:"after_#{lifecycle}", callback_object, :unless => :evaluates_to_true!) 80 | send(:"around_#{lifecycle}", callback_object) 81 | send(:"before_#{lifecycle}", other_callback_object) 82 | 83 | define_method(:shake!){} 84 | define_method(:dance!){} 85 | define_method(:giggle!){} 86 | end.new 87 | end 88 | context "as a simple callback test" do 89 | it "should find the callback before the fact" do 90 | expect(@model).to callback(:dance!).before(lifecycle) 91 | end 92 | it "should find the callback after the fact" do 93 | expect(@model).to callback(:shake!).after(lifecycle) 94 | end 95 | it "should find the callback around the fact" do 96 | expect(@model).to callback(:giggle!).around(lifecycle) 97 | end 98 | it "should not find callbacks that are not there" do 99 | expect(@model).not_to callback(:scream!).around(lifecycle) 100 | end 101 | it "should not find callback_objects around the fact" do 102 | expect(@model).not_to callback(:shake!).around(lifecycle) 103 | end 104 | it "should have a meaningful description" do 105 | matcher = callback(:dance!).before(lifecycle) 106 | expect(matcher.description).to eq("callback dance! before #{lifecycle}") 107 | end 108 | it "should find the callback_object before the fact" do 109 | expect(@model).to callback(@callback_object_class).before(lifecycle) 110 | end 111 | it "should find the callback_object after the fact" do 112 | expect(@model).to callback(@callback_object_class).after(lifecycle) 113 | end 114 | it "should find the callback_object around the fact" do 115 | expect(@model).to callback(@callback_object_class).around(lifecycle) 116 | end 117 | it "should not find callbacks that are not there" do 118 | expect(@model).not_to callback(@callback_object_not_found_class).around(lifecycle) 119 | end 120 | it "should not find callback_objects around the fact" do 121 | expect(@model).not_to callback(@callback_object_not_found_class).around(lifecycle) 122 | end 123 | it "should have a meaningful description" do 124 | matcher = callback(@callback_object_class).before(lifecycle) 125 | expect(matcher.description).to eq("callback Callback before #{lifecycle}") 126 | end 127 | it "should have a meaningful error if it fails with an inexistent method on a model" do 128 | matcher = callback(:wiggle!).before(lifecycle) 129 | expect(matcher.matches?(@model)).to eq(false) 130 | expect(matcher.failure_message).to eq("callback wiggle! is listed as a callback before #{lifecycle}, but the model does not respond to wiggle! (using respond_to?(:wiggle!, true)") 131 | end 132 | it "should have a meaningful error if it fails with an inexistent method on a callback class" do 133 | matcher = callback(@other_callback_object_class).before(lifecycle) 134 | expect(matcher.matches?(@model)).to eq(false) 135 | expect(matcher.failure_message).to eq("callback OtherCallback is listed as a callback before #{lifecycle}, but the given object does not respond to before_#{lifecycle} (using respond_to?(:before_#{lifecycle}, true)") 136 | end 137 | end 138 | context "with conditions" do 139 | it "should match the if condition" do 140 | expect(@model).to callback(:dance!).before(lifecycle).if(:evaluates_to_false!) 141 | end 142 | it "should match the unless condition" do 143 | expect(@model).to callback(:shake!).after(lifecycle).unless(:evaluates_to_true!) 144 | end 145 | it "should not find callbacks not matching the conditions" do 146 | expect(@model).not_to callback(:giggle!).around(lifecycle).unless(:evaluates_to_false!) 147 | end 148 | it "should not find callbacks that are not there entirely" do 149 | expect(@model).not_to callback(:scream!).before(lifecycle).unless(:evaluates_to_false!) 150 | end 151 | it "should have a meaningful description" do 152 | matcher = callback(:dance!).after(lifecycle).unless(:evaluates_to_false!) 153 | expect(matcher.description).to eq("callback dance! after #{lifecycle} unless evaluates_to_false! evaluates to false") 154 | end 155 | 156 | it "should match the if condition" do 157 | expect(@model).to callback(@callback_object_class).before(lifecycle).if(:evaluates_to_false!) 158 | end 159 | it "should match the unless condition" do 160 | expect(@model).to callback(@callback_object_class).after(lifecycle).unless(:evaluates_to_true!) 161 | end 162 | it "should not find callbacks not matching the conditions" do 163 | expect(@model).not_to callback(@callback_object_class).around(lifecycle).unless(:evaluates_to_false!) 164 | end 165 | it "should not find callbacks that are not there entirely" do 166 | expect(@model).not_to callback(@callback_object_not_found_class).before(lifecycle).unless(:evaluates_to_false!) 167 | end 168 | it "should have a meaningful description" do 169 | matcher = callback(@callback_object_class).after(lifecycle).unless(:evaluates_to_false!) 170 | expect(matcher.description).to eq("callback Callback after #{lifecycle} unless evaluates_to_false! evaluates to false") 171 | end 172 | end 173 | end 174 | end 175 | 176 | context "on validation" do 177 | before do 178 | @callback_object_class = define_model(:callback) do 179 | define_method("before_validation"){} 180 | define_method("after_validation"){} 181 | end 182 | 183 | @callback_object_class2 = define_model(:callback2) do 184 | define_method("before_validation"){} 185 | define_method("after_validation"){} 186 | end 187 | 188 | callback_object = @callback_object_class.new 189 | callback_object2 = @callback_object_class2.new 190 | 191 | @callback_object_not_found_class = define_model(:callback_not_found) do 192 | define_method("before_validation"){} 193 | define_method("after_validation"){} 194 | end 195 | @model = define_model(:example, :attr => :string, 196 | :other => :integer) do 197 | before_validation :dance!, :if => :evaluates_to_false! 198 | after_validation :shake!, :unless => :evaluates_to_true! 199 | before_validation :dress!, :on => :create 200 | after_validation :shriek!, :on => :update, :unless => :evaluates_to_true! 201 | after_validation :pucker!, :on => :save, :if => :evaluates_to_false! 202 | before_validation callback_object, :if => :evaluates_to_false! 203 | after_validation callback_object, :unless => :evaluates_to_true! 204 | before_validation callback_object, :on => :create 205 | after_validation callback_object, :on => :update, :unless => :evaluates_to_true! 206 | after_validation callback_object2, :on => :save, :if => :evaluates_to_false! 207 | define_method(:dance!){} 208 | define_method(:shake!){} 209 | define_method(:dress!){} 210 | define_method(:shriek!){} 211 | define_method(:pucker!){} 212 | end.new 213 | end 214 | 215 | context "as a simple callback test" do 216 | it "should find the callback before the fact" do 217 | expect(@model).to callback(:dance!).before(:validation) 218 | end 219 | it "should find the callback after the fact" do 220 | expect(@model).to callback(:shake!).after(:validation) 221 | end 222 | it "should not find a callback around the fact" do 223 | expect(@model).not_to callback(:giggle!).around(:validation) 224 | end 225 | it "should not find callbacks that are not there" do 226 | expect(@model).not_to callback(:scream!).around(:validation) 227 | end 228 | it "should have a meaningful description" do 229 | matcher = callback(:dance!).before(:validation) 230 | expect(matcher.description).to eq("callback dance! before validation") 231 | end 232 | 233 | it "should find the callback before the fact" do 234 | expect(@model).to callback(@callback_object_class).before(:validation) 235 | end 236 | it "should find the callback after the fact" do 237 | expect(@model).to callback(@callback_object_class).after(:validation) 238 | end 239 | it "should not find a callback around the fact" do 240 | expect(@model).not_to callback(@callback_object_class).around(:validation) 241 | end 242 | it "should not find callbacks that are not there" do 243 | expect(@model).not_to callback(@callback_object_not_found_class).around(:validation) 244 | end 245 | it "should have a meaningful description" do 246 | matcher = callback(@callback_object_class).before(:validation) 247 | expect(matcher.description).to eq("callback Callback before validation") 248 | end 249 | end 250 | 251 | context "with additinal lifecycles defined" do 252 | it "should find the callback before the fact on create" do 253 | expect(@model).to callback(:dress!).before(:validation).on(:create) 254 | end 255 | it "should find the callback after the fact on update" do 256 | expect(@model).to callback(:shriek!).after(:validation).on(:update) 257 | end 258 | it "should find the callback after the fact on save" do 259 | expect(@model).to callback(:pucker!).after(:validation).on(:save) 260 | end 261 | it "should not find a callback for pucker! after the fact on update" do 262 | expect(@model).not_to callback(:pucker!).after(:validation).on(:update) 263 | end 264 | it "should have a meaningful description" do 265 | matcher = callback(:dance!).after(:validation).on(:update) 266 | expect(matcher.description).to eq("callback dance! after validation on update") 267 | end 268 | 269 | it "should find the callback before the fact on create" do 270 | expect(@model).to callback(@callback_object_class).before(:validation).on(:create) 271 | end 272 | it "should find the callback after the fact on update" do 273 | expect(@model).to callback(@callback_object_class).after(:validation).on(:update) 274 | end 275 | it "should find the callback after the fact on save" do 276 | expect(@model).to callback(@callback_object_class2).after(:validation).on(:save) 277 | end 278 | it "should not find a callback for Callback after the fact on update" do 279 | expect(@model).not_to callback(@callback_object_class2).after(:validation).on(:update) 280 | end 281 | it "should have a meaningful description" do 282 | matcher = callback(@callback_object_class).after(:validation).on(:update) 283 | expect(matcher.description).to eq("callback Callback after validation on update") 284 | end 285 | end 286 | 287 | context "with conditions" do 288 | it "should match the if condition" do 289 | expect(@model).to callback(:dance!).before(:validation).if(:evaluates_to_false!) 290 | end 291 | it "should match the unless condition" do 292 | expect(@model).to callback(:shake!).after(:validation).unless(:evaluates_to_true!) 293 | end 294 | it "should not find callbacks not matching the conditions" do 295 | expect(@model).not_to callback(:giggle!).around(:validation).unless(:evaluates_to_false!) 296 | end 297 | it "should not find callbacks that are not there entirely" do 298 | expect(@model).not_to callback(:scream!).before(:validation).unless(:evaluates_to_false!) 299 | end 300 | it "should have a meaningful description" do 301 | matcher = callback(:dance!).after(:validation).unless(:evaluates_to_false!) 302 | expect(matcher.description).to eq("callback dance! after validation unless evaluates_to_false! evaluates to false") 303 | end 304 | 305 | it "should match the if condition" do 306 | expect(@model).to callback(@callback_object_class).before(:validation).if(:evaluates_to_false!) 307 | end 308 | it "should match the unless condition" do 309 | expect(@model).to callback(@callback_object_class).after(:validation).unless(:evaluates_to_true!) 310 | end 311 | it "should not find callbacks not matching the conditions" do 312 | expect(@model).not_to callback(@callback_object_class).around(:validation).unless(:evaluates_to_false!) 313 | end 314 | it "should not find callbacks that are not there entirely" do 315 | expect(@model).not_to callback(@callback_object_not_found_class).before(:validation).unless(:evaluates_to_false!) 316 | end 317 | it "should have a meaningful description" do 318 | matcher = callback(@callback_object_class).after(:validation).unless(:evaluates_to_false!) 319 | expect(matcher.description).to eq("callback Callback after validation unless evaluates_to_false! evaluates to false") 320 | end 321 | end 322 | 323 | context "with conditions and additional lifecycles" do 324 | it "should find the callback before the fact on create" do 325 | expect(@model).to callback(:dress!).before(:validation).on(:create) 326 | end 327 | it "should find the callback after the fact on update with the unless condition" do 328 | expect(@model).to callback(:shriek!).after(:validation).on(:update).unless(:evaluates_to_true!) 329 | end 330 | it "should find the callback after the fact on save with the if condition" do 331 | expect(@model).to callback(:pucker!).after(:validation).on(:save).if(:evaluates_to_false!) 332 | end 333 | it "should not find a callback for pucker! after the fact on save with the wrong condition" do 334 | expect(@model).not_to callback(:pucker!).after(:validation).on(:save).unless(:evaluates_to_false!) 335 | end 336 | it "should have a meaningful description" do 337 | matcher = callback(:dance!).after(:validation).on(:save).unless(:evaluates_to_false!) 338 | expect(matcher.description).to eq("callback dance! after validation on save unless evaluates_to_false! evaluates to false") 339 | end 340 | 341 | it "should find the callback before the fact on create" do 342 | expect(@model).to callback(@callback_object_class).before(:validation).on(:create) 343 | end 344 | it "should find the callback after the fact on update with the unless condition" do 345 | expect(@model).to callback(@callback_object_class).after(:validation).on(:update).unless(:evaluates_to_true!) 346 | end 347 | it "should find the callback after the fact on save with the if condition" do 348 | expect(@model).to callback(@callback_object_class2).after(:validation).on(:save).if(:evaluates_to_false!) 349 | end 350 | it "should not find a callback for Callback after the fact on save with the wrong condition" do 351 | expect(@model).not_to callback(@callback_object_class).after(:validation).on(:save).unless(:evaluates_to_false!) 352 | end 353 | it "should have a meaningful description" do 354 | matcher = callback(@callback_object_class).after(:validation).on(:save).unless(:evaluates_to_false!) 355 | expect(matcher.description).to eq("callback Callback after validation on save unless evaluates_to_false! evaluates to false") 356 | end 357 | end 358 | end 359 | 360 | 361 | [:rollback, :commit].each do |lifecycle| 362 | context "on #{lifecycle}" do 363 | before do 364 | @callback_object_class = define_model(:callback) do 365 | define_method("after_#{lifecycle}"){} 366 | end 367 | 368 | @callback_object_class2 = define_model(:callback2) do 369 | define_method("after_#{lifecycle}"){} 370 | end 371 | 372 | callback_object = @callback_object_class.new 373 | callback_object2 = @callback_object_class2.new 374 | 375 | @callback_object_not_found_class = define_model(:callback_not_found) do 376 | define_method("after_#{lifecycle}"){} 377 | end 378 | @model = define_model(:example, :attr => :string, 379 | :other => :integer) do 380 | send :"after_#{lifecycle}", :dance!, :if => :evaluates_to_false! 381 | send :"after_#{lifecycle}", :shake!, :unless => :evaluates_to_true! 382 | send :"after_#{lifecycle}", :dress!, :on => :create 383 | send :"after_#{lifecycle}", :shriek!, :on => :update, :unless => :evaluates_to_true! 384 | send :"after_#{lifecycle}", :pucker!, :on => :destroy, :if => :evaluates_to_false! 385 | if rails_version >= '3.2' 386 | send :"after_#{lifecycle}", :jump!, :on => [:create, :update] 387 | send :"after_#{lifecycle}", :holler!, :on => [:update, :destroy] 388 | end 389 | send :"after_#{lifecycle}", callback_object, :if => :evaluates_to_false! 390 | send :"after_#{lifecycle}", callback_object, :unless => :evaluates_to_true! 391 | send :"after_#{lifecycle}", callback_object, :on => :create 392 | send :"after_#{lifecycle}", callback_object, :on => :update, :unless => :evaluates_to_true! 393 | send :"after_#{lifecycle}", callback_object2, :on => :destroy, :if => :evaluates_to_false! 394 | if rails_version >= '3.2' 395 | send :"after_#{lifecycle}", callback_object, :on => [:create, :update] 396 | send :"after_#{lifecycle}", callback_object, :on => [:update, :destroy] 397 | end 398 | 399 | define_method(:dance!){} 400 | define_method(:shake!){} 401 | define_method(:dress!){} 402 | define_method(:shriek!){} 403 | define_method(:pucker!){} 404 | define_method(:jump!){} 405 | define_method(:holler!){} 406 | end.new 407 | end 408 | 409 | context "as a simple callback test" do 410 | it "should find the callback after the fact" do 411 | expect(@model).to callback(:shake!).after(lifecycle) 412 | end 413 | it "should not find callbacks that are not there" do 414 | expect(@model).not_to callback(:scream!).after(lifecycle) 415 | end 416 | it "should have a meaningful description" do 417 | matcher = callback(:dance!).after(lifecycle) 418 | expect(matcher.description).to eq("callback dance! after #{lifecycle}") 419 | end 420 | 421 | it "should find the callback after the fact" do 422 | expect(@model).to callback(@callback_object_class).after(lifecycle) 423 | end 424 | it "should not find callbacks that are not there" do 425 | expect(@model).not_to callback(@callback_object_not_found_class).after(lifecycle) 426 | end 427 | it "should have a meaningful description" do 428 | matcher = callback(@callback_object_class).after(lifecycle) 429 | expect(matcher.description).to eq("callback Callback after #{lifecycle}") 430 | end 431 | end 432 | 433 | context "with additinal lifecycles defined" do 434 | it "should find the callback after the fact on create" do 435 | expect(@model).to callback(:dress!).after(lifecycle).on(:create) 436 | end 437 | it "should find the callback after the fact on update" do 438 | expect(@model).to callback(:shriek!).after(lifecycle).on(:update) 439 | end 440 | it "should find the callback after the fact on save" do 441 | expect(@model).to callback(:pucker!).after(lifecycle).on(:destroy) 442 | end 443 | it "should not find a callback for pucker! after the fact on update" do 444 | expect(@model).not_to callback(:pucker!).after(lifecycle).on(:update) 445 | end 446 | it "should have a meaningful description" do 447 | matcher = callback(:dance!).after(lifecycle).on(:update) 448 | expect(matcher.description).to eq("callback dance! after #{lifecycle} on update") 449 | end 450 | 451 | it "should find the callback before the fact on create" do 452 | expect(@model).to callback(@callback_object_class).after(lifecycle).on(:create) 453 | end 454 | it "should find the callback after the fact on update" do 455 | expect(@model).to callback(@callback_object_class).after(lifecycle).on(:update) 456 | end 457 | it "should find the callback after the fact on save" do 458 | expect(@model).to callback(@callback_object_class2).after(lifecycle).on(:destroy) 459 | end 460 | it "should not find a callback for Callback after the fact on update" do 461 | expect(@model).not_to callback(@callback_object_class2).after(lifecycle).on(:update) 462 | end 463 | it "should have a meaningful description" do 464 | matcher = callback(@callback_object_class).after(lifecycle).on(:update) 465 | expect(matcher.description).to eq("callback Callback after #{lifecycle} on update") 466 | end 467 | end 468 | 469 | context "with multiple lifecycles defined", :"rails_3.2" => true do 470 | it "should find the callback after the fact on create and update" do 471 | expect(@model).to callback(:jump!).after(lifecycle).on([:create, :update]) 472 | end 473 | it "should find the callback after the fact on update and destroy" do 474 | expect(@model).to callback(:holler!).after(lifecycle).on([:update, :destroy]) 475 | end 476 | 477 | it "should find the callback after the fact on create and update" do 478 | expect(@model).to callback(@callback_object_class).after(lifecycle).on([:create, :update]) 479 | end 480 | it "should find the callback after the fact on update and destroy" do 481 | expect(@model).to callback(@callback_object_class).after(lifecycle).on([:update, :destroy]) 482 | end 483 | end 484 | 485 | context "with conditions" do 486 | it "should match the if condition" do 487 | expect(@model).to callback(:dance!).after(lifecycle).if(:evaluates_to_false!) 488 | end 489 | it "should match the unless condition" do 490 | expect(@model).to callback(:shake!).after(lifecycle).unless(:evaluates_to_true!) 491 | end 492 | it "should not find callbacks not matching the conditions" do 493 | expect(@model).not_to callback(:giggle!).after(lifecycle).unless(:evaluates_to_false!) 494 | end 495 | it "should not find callbacks that are not there entirely" do 496 | expect(@model).not_to callback(:scream!).after(lifecycle).unless(:evaluates_to_false!) 497 | end 498 | it "should have a meaningful description" do 499 | matcher = callback(:dance!).after(lifecycle).unless(:evaluates_to_false!) 500 | expect(matcher.description).to eq("callback dance! after #{lifecycle} unless evaluates_to_false! evaluates to false") 501 | end 502 | 503 | it "should match the if condition" do 504 | expect(@model).to callback(@callback_object_class).after(lifecycle).if(:evaluates_to_false!) 505 | end 506 | it "should match the unless condition" do 507 | expect(@model).to callback(@callback_object_class).after(lifecycle).unless(:evaluates_to_true!) 508 | end 509 | it "should not find callbacks not matching the conditions" do 510 | expect(@model).not_to callback(@callback_object_class).after(lifecycle).unless(:evaluates_to_false!) 511 | end 512 | it "should not find callbacks that are not there entirely" do 513 | expect(@model).not_to callback(@callback_object_not_found_class).after(lifecycle).unless(:evaluates_to_false!) 514 | end 515 | it "should have a meaningful description" do 516 | matcher = callback(@callback_object_class).after(lifecycle).unless(:evaluates_to_false!) 517 | expect(matcher.description).to eq("callback Callback after #{lifecycle} unless evaluates_to_false! evaluates to false") 518 | end 519 | end 520 | 521 | context "with conditions and additional lifecycles" do 522 | it "should find the callback before the fact on create" do 523 | expect(@model).to callback(:dress!).after(lifecycle).on(:create) 524 | end 525 | it "should find the callback after the fact on update with the unless condition" do 526 | expect(@model).to callback(:shriek!).after(lifecycle).on(:update).unless(:evaluates_to_true!) 527 | end 528 | it "should find the callback after the fact on save with the if condition" do 529 | expect(@model).to callback(:pucker!).after(lifecycle).on(:destroy).if(:evaluates_to_false!) 530 | end 531 | it "should not find a callback for pucker! after the fact on save with the wrong condition" do 532 | expect(@model).not_to callback(:pucker!).after(lifecycle).on(:destroy).unless(:evaluates_to_false!) 533 | end 534 | it "should have a meaningful description" do 535 | matcher = callback(:dance!).after(lifecycle).on(:save).unless(:evaluates_to_false!) 536 | expect(matcher.description).to eq("callback dance! after #{lifecycle} on save unless evaluates_to_false! evaluates to false") 537 | end 538 | 539 | it "should find the callback before the fact on create" do 540 | expect(@model).to callback(@callback_object_class).after(lifecycle).on(:create) 541 | end 542 | it "should find the callback after the fact on update with the unless condition" do 543 | expect(@model).to callback(@callback_object_class).after(lifecycle).on(:update).unless(:evaluates_to_true!) 544 | end 545 | it "should find the callback after the fact on save with the if condition" do 546 | expect(@model).to callback(@callback_object_class2).after(lifecycle).on(:destroy).if(:evaluates_to_false!) 547 | end 548 | it "should not find a callback for Callback after the fact on save with the wrong condition" do 549 | expect(@model).not_to callback(@callback_object_class).after(lifecycle).on(:destroy).unless(:evaluates_to_false!) 550 | end 551 | it "should have a meaningful description" do 552 | matcher = callback(@callback_object_class).after(lifecycle).on(:destroy).unless(:evaluates_to_false!) 553 | expect(matcher.description).to eq("callback Callback after #{lifecycle} on destroy unless evaluates_to_false! evaluates to false") 554 | end 555 | end 556 | end 557 | end 558 | 559 | [:initialize, :find, :touch].each do |lifecycle| 560 | context "on #{lifecycle}" do 561 | before do 562 | 563 | @callback_object_class = define_model(:callback) do 564 | define_method("after_#{lifecycle}"){} 565 | end 566 | @callback_object_class2 = define_model(:callback2) do 567 | define_method("after_#{lifecycle}"){} 568 | end 569 | 570 | callback_object = @callback_object_class.new 571 | callback_object2 = @callback_object_class2.new 572 | 573 | @callback_object_not_found_class = define_model(:callback_not_found) do 574 | define_method("after_#{lifecycle}"){} 575 | end 576 | 577 | @model = define_model(:example, :attr => :string, 578 | :other => :integer) do 579 | send(:"after_#{lifecycle}", :dance!, :if => :evaluates_to_false!) 580 | send(:"after_#{lifecycle}", :shake!, :unless => :evaluates_to_true!) 581 | send(:"after_#{lifecycle}", callback_object, :if => :evaluates_to_false!) 582 | send(:"after_#{lifecycle}", callback_object2, :unless => :evaluates_to_true!) 583 | define_method(:shake!){} 584 | define_method(:dance!){} 585 | 586 | define_method :evaluates_to_false! do 587 | false 588 | end 589 | 590 | define_method :evaluates_to_true! do 591 | true 592 | end 593 | 594 | end.new 595 | end 596 | 597 | context "as a simple callback test" do 598 | it "should not find a callback before the fact" do 599 | expect(@model).not_to callback(:dance!).before(lifecycle) 600 | end 601 | it "should find the callback after the fact" do 602 | expect(@model).to callback(:shake!).after(lifecycle) 603 | end 604 | it "should not find a callback around the fact" do 605 | expect(@model).not_to callback(:giggle!).around(lifecycle) 606 | end 607 | it "should not find callbacks that are not there" do 608 | expect(@model).not_to callback(:scream!).around(lifecycle) 609 | end 610 | it "should have a meaningful description" do 611 | matcher = callback(:dance!).before(lifecycle) 612 | expect(matcher.description).to eq("callback dance! before #{lifecycle}") 613 | end 614 | 615 | it "should not find a callback before the fact" do 616 | expect(@model).not_to callback(@callback_object_class).before(lifecycle) 617 | end 618 | it "should find the callback after the fact" do 619 | expect(@model).to callback(@callback_object_class).after(lifecycle) 620 | end 621 | it "should not find a callback around the fact" do 622 | expect(@model).not_to callback(@callback_object_class).around(lifecycle) 623 | end 624 | it "should not find callbacks that are not there" do 625 | expect(@model).not_to callback(@callback_object_not_found_class).around(lifecycle) 626 | end 627 | it "should have a meaningful description" do 628 | matcher = callback(@callback_object_class).before(lifecycle) 629 | expect(matcher.description).to eq("callback Callback before #{lifecycle}") 630 | end 631 | end 632 | 633 | context "with conditions" do 634 | it "should match the if condition" do 635 | expect(@model).to callback(:dance!).after(lifecycle).if(:evaluates_to_false!) 636 | end 637 | it "should match the unless condition" do 638 | expect(@model).to callback(:shake!).after(lifecycle).unless(:evaluates_to_true!) 639 | end 640 | it "should not find callbacks not matching the conditions" do 641 | expect(@model).not_to callback(:giggle!).around(lifecycle).unless(:evaluates_to_false!) 642 | end 643 | it "should not find callbacks that are not there entirely" do 644 | expect(@model).not_to callback(:scream!).before(lifecycle).unless(:evaluates_to_false!) 645 | end 646 | it "should have a meaningful description" do 647 | matcher = callback(:dance!).after(lifecycle).unless(:evaluates_to_false!) 648 | expect(matcher.description).to eq("callback dance! after #{lifecycle} unless evaluates_to_false! evaluates to false") 649 | end 650 | it "should match the if condition" do 651 | expect(@model).to callback(@callback_object_class).after(lifecycle).if(:evaluates_to_false!) 652 | end 653 | it "should match the unless condition" do 654 | expect(@model).to callback(@callback_object_class2).after(lifecycle).unless(:evaluates_to_true!) 655 | end 656 | it "should not find callbacks not matching the conditions" do 657 | expect(@model).not_to callback(@callback_object_class).around(lifecycle).unless(:evaluates_to_false!) 658 | end 659 | it "should not find callbacks that are not there entirely" do 660 | expect(@model).not_to callback(@callback_object_not_found_class).before(lifecycle).unless(:evaluates_to_false!) 661 | end 662 | it "should have a meaningful description" do 663 | matcher = callback(@callback_object_class).after(lifecycle).unless(:evaluates_to_false!) 664 | expect(matcher.description).to eq("callback Callback after #{lifecycle} unless evaluates_to_false! evaluates to false") 665 | end 666 | end 667 | 668 | end 669 | end 670 | end 671 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | LOGGER = Logger.new STDOUT 4 | TESTAPP_ROOT = Pathname.new File.expand_path('../tmp/aruba/testapp', __FILE__) 5 | FileUtils.rm_rf TESTAPP_ROOT if File.exists? TESTAPP_ROOT 6 | 7 | ENV['RAILS_ENV'] = 'test' 8 | ENV['BUNDLE_GEMFILE'] ||= TESTAPP_ROOT.join('Gemfile') 9 | 10 | LOGGER.info "Generating Rails app in #{TESTAPP_ROOT}..." 11 | `rails new #{TESTAPP_ROOT}` 12 | LOGGER.info "Done" 13 | 14 | require TESTAPP_ROOT.join('config', 'environment') 15 | require 'shoulda-callback-matchers' 16 | require 'rspec/rails' 17 | 18 | PROJECT_ROOT = Pathname.new File.expand_path('../..', __FILE__) 19 | $LOAD_PATH << PROJECT_ROOT.join('lib') 20 | 21 | Dir[PROJECT_ROOT.join('spec', 'support', '**', '*.rb')].each do |file| 22 | require file 23 | end 24 | 25 | # Run the migrations 26 | LOGGER.info "Running the migrations for the testapp..." 27 | ActiveRecord::Migration.verbose = false 28 | ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate") 29 | LOGGER.info "Done" 30 | 31 | RSpec.configure do |config| 32 | config.include ClassBuilder 33 | config.include ModelBuilder 34 | if rails_version < '3.2' 35 | config.filter_run_excluding :"rails_3.2" => true 36 | end 37 | 38 | # rspec-rails 3 will no longer automatically infer an example group's spec type 39 | # from the file location. You can explicitly opt-in to the feature using this 40 | # config option. 41 | # To explicitly tag specs without using automatic inference, set the `:type` 42 | # metadata manually: 43 | # 44 | # describe ThingsController, :type => :controller do 45 | # # Equivalent to being in spec/controllers 46 | # end 47 | config.infer_spec_type_from_file_location! 48 | end -------------------------------------------------------------------------------- /spec/support/class_builder.rb: -------------------------------------------------------------------------------- 1 | module ClassBuilder 2 | def self.included example_group 3 | example_group.class_eval do 4 | after do 5 | teardown_defined_constants 6 | end 7 | end 8 | end 9 | 10 | def define_class class_name, base = Object, &block 11 | Object.const_set class_name, Class.new(base) 12 | 13 | Object.const_get(class_name).tap do |constant_class| 14 | constant_class.unloadable 15 | 16 | if block_given? 17 | constant_class.class_eval(&block) 18 | end 19 | 20 | if constant_class.respond_to?(:reset_column_information) 21 | constant_class.reset_column_information 22 | end 23 | end 24 | end 25 | 26 | def teardown_defined_constants 27 | ActiveSupport::Dependencies.clear 28 | end 29 | end -------------------------------------------------------------------------------- /spec/support/model_builder.rb: -------------------------------------------------------------------------------- 1 | module ModelBuilder 2 | def self.included(example_group) 3 | example_group.class_eval do 4 | before do 5 | @created_tables ||= [] 6 | end 7 | 8 | after do 9 | drop_created_tables 10 | end 11 | end 12 | end 13 | 14 | def create_table(table_name, options = {}, &block) 15 | connection.create_table(table_name, options, &block) 16 | @created_tables << table_name 17 | connection 18 | rescue Exception => e 19 | drop_table(table_name) 20 | raise e 21 | end 22 | 23 | def define_model_class(class_name, &block) 24 | define_class(class_name, ActiveRecord::Base, &block) 25 | end 26 | 27 | def define_active_model_class(class_name, options = {}, &block) 28 | define_class(class_name) do 29 | include ActiveModel::Validations 30 | extend ActiveModel::Callbacks 31 | define_model_callbacks :initialize, :find, :touch, :only => :after 32 | define_model_callbacks :save, :create, :update, :destroy 33 | 34 | options[:accessors].each do |column| 35 | attr_accessor column.to_sym 36 | end 37 | 38 | if block_given? 39 | class_eval(&block) 40 | end 41 | end 42 | end 43 | 44 | def define_model(name, columns = {}, &block) 45 | class_name = name.to_s.pluralize.classify 46 | table_name = class_name.tableize 47 | 48 | create_table(table_name) do |table| 49 | columns.each do |name, type| 50 | table.column name, type 51 | end 52 | end 53 | 54 | define_model_class(class_name, &block) 55 | end 56 | 57 | def drop_created_tables 58 | @created_tables.each do |table_name| 59 | drop_table(table_name) 60 | end 61 | end 62 | 63 | def drop_table table_name 64 | connection.execute("DROP TABLE IF EXISTS #{table_name}") 65 | end 66 | 67 | def connection 68 | @connection ||= ActiveRecord::Base.connection 69 | end 70 | end --------------------------------------------------------------------------------