├── .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 | [](http://badge.fury.io/rb/shoulda-callback-matchers)[](https://travis-ci.org/jdliss/shoulda-callback-matchers) [](https://codeclimate.com/github/beatrichartz/shoulda-callback-matchers) [](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
--------------------------------------------------------------------------------