jcnetdev-shoulda 4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. data/CONTRIBUTION_GUIDELINES.rdoc +12 -0
  2. data/MIT-LICENSE +22 -0
  3. data/README.rdoc +123 -0
  4. data/Rakefile +32 -0
  5. data/bin/convert_to_should_syntax +40 -0
  6. data/init.rb +1 -0
  7. data/lib/shoulda.rb +43 -0
  8. data/lib/shoulda/active_record_helpers.rb +670 -0
  9. data/lib/shoulda/color.rb +77 -0
  10. data/lib/shoulda/controller_tests/controller_tests.rb +467 -0
  11. data/lib/shoulda/controller_tests/formats/html.rb +201 -0
  12. data/lib/shoulda/controller_tests/formats/xml.rb +170 -0
  13. data/lib/shoulda/gem/proc_extensions.rb +14 -0
  14. data/lib/shoulda/gem/shoulda.rb +246 -0
  15. data/lib/shoulda/general.rb +118 -0
  16. data/lib/shoulda/private_helpers.rb +22 -0
  17. data/rails/init.rb +1 -0
  18. data/shoulda.gemspec +109 -0
  19. data/tasks/list_tests.rake +23 -0
  20. data/tasks/yaml_to_shoulda.rake +28 -0
  21. data/test/README +36 -0
  22. data/test/fixtures/addresses.yml +3 -0
  23. data/test/fixtures/posts.yml +5 -0
  24. data/test/fixtures/taggings.yml +0 -0
  25. data/test/fixtures/tags.yml +9 -0
  26. data/test/fixtures/users.yml +6 -0
  27. data/test/functional/posts_controller_test.rb +43 -0
  28. data/test/functional/users_controller_test.rb +36 -0
  29. data/test/other/context_test.rb +115 -0
  30. data/test/other/helpers_test.rb +80 -0
  31. data/test/other/private_helpers_test.rb +26 -0
  32. data/test/rails_root/app/controllers/application.rb +25 -0
  33. data/test/rails_root/app/controllers/posts_controller.rb +78 -0
  34. data/test/rails_root/app/controllers/users_controller.rb +81 -0
  35. data/test/rails_root/app/helpers/application_helper.rb +3 -0
  36. data/test/rails_root/app/helpers/posts_helper.rb +2 -0
  37. data/test/rails_root/app/helpers/users_helper.rb +2 -0
  38. data/test/rails_root/app/models/address.rb +4 -0
  39. data/test/rails_root/app/models/dog.rb +4 -0
  40. data/test/rails_root/app/models/flea.rb +3 -0
  41. data/test/rails_root/app/models/post.rb +11 -0
  42. data/test/rails_root/app/models/tag.rb +8 -0
  43. data/test/rails_root/app/models/tagging.rb +4 -0
  44. data/test/rails_root/app/models/user.rb +17 -0
  45. data/test/rails_root/app/views/layouts/posts.rhtml +17 -0
  46. data/test/rails_root/app/views/layouts/users.rhtml +17 -0
  47. data/test/rails_root/app/views/posts/edit.rhtml +27 -0
  48. data/test/rails_root/app/views/posts/index.rhtml +25 -0
  49. data/test/rails_root/app/views/posts/new.rhtml +26 -0
  50. data/test/rails_root/app/views/posts/show.rhtml +18 -0
  51. data/test/rails_root/app/views/users/edit.rhtml +22 -0
  52. data/test/rails_root/app/views/users/index.rhtml +22 -0
  53. data/test/rails_root/app/views/users/new.rhtml +21 -0
  54. data/test/rails_root/app/views/users/show.rhtml +13 -0
  55. data/test/rails_root/config/boot.rb +109 -0
  56. data/test/rails_root/config/database.yml +4 -0
  57. data/test/rails_root/config/environment.rb +18 -0
  58. data/test/rails_root/config/environments/sqlite3.rb +0 -0
  59. data/test/rails_root/config/initializers/new_rails_defaults.rb +15 -0
  60. data/test/rails_root/config/routes.rb +6 -0
  61. data/test/rails_root/db/migrate/001_create_users.rb +17 -0
  62. data/test/rails_root/db/migrate/002_create_posts.rb +13 -0
  63. data/test/rails_root/db/migrate/003_create_taggings.rb +12 -0
  64. data/test/rails_root/db/migrate/004_create_tags.rb +11 -0
  65. data/test/rails_root/db/migrate/005_create_dogs.rb +11 -0
  66. data/test/rails_root/db/migrate/006_create_addresses.rb +13 -0
  67. data/test/rails_root/db/migrate/007_create_fleas.rb +11 -0
  68. data/test/rails_root/db/migrate/008_create_dogs_fleas.rb +12 -0
  69. data/test/rails_root/db/migrate/009_add_ssn_to_users.rb +9 -0
  70. data/test/rails_root/db/schema.rb +0 -0
  71. data/test/rails_root/log/.keep +0 -0
  72. data/test/rails_root/public/.htaccess +40 -0
  73. data/test/rails_root/public/404.html +30 -0
  74. data/test/rails_root/public/422.html +30 -0
  75. data/test/rails_root/public/500.html +30 -0
  76. data/test/rails_root/script/console +3 -0
  77. data/test/rails_root/script/generate +3 -0
  78. data/test/rails_root/vendor/plugins/.keep +0 -0
  79. data/test/test_helper.rb +35 -0
  80. data/test/unit/address_test.rb +7 -0
  81. data/test/unit/dog_test.rb +7 -0
  82. data/test/unit/flea_test.rb +7 -0
  83. data/test/unit/post_test.rb +14 -0
  84. data/test/unit/tag_test.rb +12 -0
  85. data/test/unit/tagging_test.rb +8 -0
  86. data/test/unit/user_test.rb +32 -0
  87. metadata +148 -0
@@ -0,0 +1,12 @@
1
+ We're using GitHub[http://github.com/thoughtbot/shoulda/tree/master] and Lighthouse[http://thoughtbot.lighthouseapp.com/projects/5807], and we've been getting any combination of github pull requests, Lighthouse tickets, patches, emails, etc. We need to normalize this workflow to make sure we don't miss any fixes.
2
+
3
+ * Make sure you're accessing the source from the {official repository}[http://github.com/thoughtbot/shoulda/tree/master].
4
+ * We prefer git branches over patches, but we can take either.
5
+ * If you're using git, please make a branch for each separate contribution. We can cherry pick your commits, but pulling from a branch is easier.
6
+ * If you're submitting patches, please cut each fix or feature into a separate patch.
7
+ * There should be a Lighthouse[http://thoughtbot.lighthouseapp.com/projects/5807] ticket for any submission. If you've found a bug and want to fix it, open a new ticket at the same time.
8
+ * We've got github/lighthouse integration going, so you can update tickets when you commit. {This blog post}[http://hoth.entp.com/2008/4/11/github-and-lighthouse-sitting-in-a-tree] explains the commit options pretty well.
9
+ * Please <b>don't send pull requests</b> Just update the lighthouse ticket with the url for your fix (or attach the patch) when it's ready. The github pull requests pretty much get dropped on the floor until someone with commit rights notices them in the mailbox.
10
+ * Contributions without tests won't be accepted. The file <tt>/test/README</tt> explains the testing system pretty thoroughly.
11
+
12
+
data/MIT-LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2007, Tammer Saleh, Thoughtbot, Inc.
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.
data/README.rdoc ADDED
@@ -0,0 +1,123 @@
1
+ = Shoulda - Making tests easy on the fingers and eyes
2
+
3
+ Shoulda makes it easy to write elegant, understandable, and maintainable tests. Shoulda consists of test macros, assertions, and helpers added on to the Test::Unit framework. It's fully compatible with your existing tests, and requires no retooling to use.
4
+
5
+ Helpers:: #context and #should give you rSpec like test blocks.
6
+ In addition, you get nested contexts and a much more readable syntax.
7
+ Macros:: Generate hundreds of lines of Controller and ActiveRecord tests with these powerful macros.
8
+ They get you started quickly, and can help you ensure that your application is conforming to best practices.
9
+ Assertions:: Many common rails testing idioms have been distilled into a set of useful assertions.
10
+
11
+ = Usage
12
+
13
+ === Context Helpers (ThoughtBot::Shoulda::Context)
14
+
15
+ Stop killing your fingers with all of those underscores... Name your tests with plain sentences!
16
+
17
+ class UserTest << Test::Unit::TestCase
18
+ context "A User instance" do
19
+ setup do
20
+ @user = User.find(:first)
21
+ end
22
+
23
+ should "return its full name" do
24
+ assert_equal 'John Doe', @user.full_name
25
+ end
26
+
27
+ context "with a profile" do
28
+ setup do
29
+ @user.profile = Profile.find(:first)
30
+ end
31
+
32
+ should "return true when sent #has_profile?" do
33
+ assert @user.has_profile?
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ Produces the following test methods:
40
+
41
+ "test: A User instance should return its full name."
42
+ "test: A User instance with a profile should return true when sent #has_profile?."
43
+
44
+ So readable!
45
+
46
+ === ActiveRecord Tests (ThoughtBot::Shoulda::ActiveRecord)
47
+
48
+ Quick macro tests for your ActiveRecord associations and validations:
49
+
50
+ class PostTest < Test::Unit::TestCase
51
+ load_all_fixtures
52
+
53
+ should_belong_to :user
54
+ should_have_many :tags, :through => :taggings
55
+
56
+ should_require_unique_attributes :title
57
+ should_require_attributes :body, :message => /wtf/
58
+ should_require_attributes :title
59
+ should_only_allow_numeric_values_for :user_id
60
+ end
61
+
62
+ class UserTest < Test::Unit::TestCase
63
+ load_all_fixtures
64
+
65
+ should_have_many :posts
66
+
67
+ should_not_allow_values_for :email, "blah", "b lah"
68
+ should_allow_values_for :email, "a@b.com", "asdf@asdf.com"
69
+ should_ensure_length_in_range :email, 1..100
70
+ should_ensure_value_in_range :age, 1..100
71
+ should_protect_attributes :password
72
+ end
73
+
74
+ Makes TDD so much easier.
75
+
76
+ === Controller Tests (ThoughtBot::Shoulda::Controller::ClassMethods)
77
+
78
+ Macros to test the most common controller patterns...
79
+
80
+ context "on GET to :show for first record" do
81
+ setup do
82
+ get :show, :id => 1
83
+ end
84
+
85
+ should_assign_to :user
86
+ should_respond_with :success
87
+ should_render_template :show
88
+ should_not_set_the_flash
89
+
90
+ should "do something else really cool" do
91
+ assert_equal 1, assigns(:user).id
92
+ end
93
+ end
94
+
95
+ Test entire controllers in a few lines...
96
+
97
+ class PostsControllerTest < Test::Unit::TestCase
98
+ should_be_restful do |resource|
99
+ resource.parent = :user
100
+
101
+ resource.create.params = { :title => "first post", :body => 'blah blah blah'}
102
+ resource.update.params = { :title => "changed" }
103
+ end
104
+ end
105
+
106
+ should_be_restful generates 40 tests on the fly, for both html and xml requests.
107
+
108
+ === Helpful Assertions (ThoughtBot::Shoulda::General)
109
+
110
+ More to come here, but have fun with what's there.
111
+
112
+ load_all_fixtures
113
+ assert_same_elements([:a, :b, :c], [:c, :a, :b])
114
+ assert_contains(['a', '1'], /\d/)
115
+ assert_contains(['a', '1'], 'a')
116
+
117
+ = Credits
118
+
119
+ Shoulda is maintained by {Tammer Saleh}[mailto:tsaleh@thoughtbot.com], and is funded by Thoughtbot[http://www.thoughtbot.com], inc.
120
+
121
+ = License
122
+
123
+ Shoulda is Copyright © 2006-2007 Tammer Saleh, Thoughtbot. It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file.
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ # Test::Unit::UI::VERBOSE
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.libs << 'lib'
9
+ t.pattern = 'test/{unit,functional,other}/**/*_test.rb'
10
+ t.verbose = false
11
+ end
12
+
13
+ Rake::RDocTask.new { |rdoc|
14
+ rdoc.rdoc_dir = 'doc'
15
+ rdoc.title = "Shoulda -- Making tests easy on the fingers and eyes"
16
+ rdoc.options << '--line-numbers' << '--inline-source'
17
+ rdoc.template = "#{ENV['template']}.rb" if ENV['template']
18
+ rdoc.rdoc_files.include('README.rdoc', 'CONTRIBUTION_GUIDELINES.rdoc', 'lib/**/*.rb')
19
+ }
20
+
21
+ desc 'Update documentation on website'
22
+ task :sync_docs => 'rdoc' do
23
+ `rsync -ave ssh doc/ dev@dev.thoughtbot.com:/home/dev/www/dev.thoughtbot.com/shoulda`
24
+ end
25
+
26
+ desc 'Default: run tests.'
27
+ task :default => ['test']
28
+
29
+ Dir['tasks/*.rake'].each do |f|
30
+ load f
31
+ end
32
+
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+ require 'fileutils'
3
+
4
+ def usage(msg = nil)
5
+ puts "Error: #{msg}" if msg
6
+ puts if msg
7
+ puts "Usage: #{File.basename(__FILE__)} normal_test_file.rb"
8
+ puts
9
+ puts "Will convert an existing test file with names like "
10
+ puts
11
+ puts " def test_should_do_stuff"
12
+ puts " ..."
13
+ puts " end"
14
+ puts
15
+ puts "to one using the new syntax: "
16
+ puts
17
+ puts " should \"be super cool\" do"
18
+ puts " ..."
19
+ puts " end"
20
+ puts
21
+ puts "A copy of the old file will be left under /tmp/ in case this script just seriously screws up"
22
+ puts
23
+ exit (msg ? 2 : 0)
24
+ end
25
+
26
+ usage("Wrong number of arguments.") unless ARGV.size == 1
27
+ usage("This system doesn't have a /tmp directory. wtf?") unless File.directory?('/tmp')
28
+
29
+ file = ARGV.shift
30
+ tmpfile = "/tmp/#{File.basename(file)}"
31
+ usage("File '#{file}' doesn't exist") unless File.exists?(file)
32
+
33
+ FileUtils.cp(file, tmpfile)
34
+ contents = File.read(tmpfile)
35
+ contents.gsub!(/def test_should_(.*)\s*$/, 'should "\1" do')
36
+ contents.gsub!(/def test_(.*)\s*$/, 'should "RENAME ME: test \1" do')
37
+ contents.gsub!(/should ".*" do$/) {|line| line.tr!('_', ' ')}
38
+ File.open(file, 'w') { |f| f.write(contents) }
39
+
40
+ puts "File '#{file}' has been converted to 'should' syntax. Old version has been stored in '#{tmpfile}'"
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
data/lib/shoulda.rb ADDED
@@ -0,0 +1,43 @@
1
+ require 'shoulda/gem/shoulda'
2
+ require 'shoulda/private_helpers'
3
+ require 'shoulda/general'
4
+ require 'shoulda/active_record_helpers'
5
+ require 'shoulda/controller_tests/controller_tests.rb'
6
+ require 'yaml'
7
+
8
+ shoulda_options = {}
9
+
10
+ possible_config_paths = []
11
+ possible_config_paths << File.join(ENV["HOME"], ".shoulda.conf") if ENV["HOME"]
12
+ possible_config_paths << "shoulda.conf"
13
+ possible_config_paths << File.join("test", "shoulda.conf")
14
+ possible_config_paths << File.join(RAILS_ROOT, "test", "shoulda.conf") if defined?(RAILS_ROOT)
15
+
16
+ possible_config_paths.each do |config_file|
17
+ if File.exists? config_file
18
+ shoulda_options = YAML.load_file(config_file).symbolize_keys
19
+ break
20
+ end
21
+ end
22
+
23
+ require 'shoulda/color' if shoulda_options[:color]
24
+
25
+ module Test # :nodoc: all
26
+ module Unit
27
+ class TestCase
28
+
29
+ include ThoughtBot::Shoulda::General
30
+ include ThoughtBot::Shoulda::Controller
31
+
32
+ extend ThoughtBot::Shoulda::ActiveRecord
33
+ end
34
+ end
35
+ end
36
+
37
+ module ActionController #:nodoc: all
38
+ module Integration
39
+ class Session
40
+ include ThoughtBot::Shoulda::General
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,670 @@
1
+ module ThoughtBot # :nodoc:
2
+ module Shoulda # :nodoc:
3
+ # = Macro test helpers for your active record models
4
+ #
5
+ # These helpers will test most of the validations and associations for your ActiveRecord models.
6
+ #
7
+ # class UserTest < Test::Unit::TestCase
8
+ # should_require_attributes :name, :phone_number
9
+ # should_not_allow_values_for :phone_number, "abcd", "1234"
10
+ # should_allow_values_for :phone_number, "(123) 456-7890"
11
+ #
12
+ # should_protect_attributes :password
13
+ #
14
+ # should_have_one :profile
15
+ # should_have_many :dogs
16
+ # should_have_many :messes, :through => :dogs
17
+ # should_belong_to :lover
18
+ # end
19
+ #
20
+ # For all of these helpers, the last parameter may be a hash of options.
21
+ #
22
+ module ActiveRecord
23
+ # Ensures that the model cannot be saved if one of the attributes listed is not present.
24
+ #
25
+ # Options:
26
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
27
+ # Regexp or string. Default = <tt>/blank/</tt>
28
+ #
29
+ # Example:
30
+ # should_require_attributes :name, :phone_number
31
+ #
32
+ def should_require_attributes(*attributes)
33
+ message = get_options!(attributes, :message)
34
+ message ||= /blank/
35
+ klass = model_class
36
+
37
+ attributes.each do |attribute|
38
+ should "require #{attribute} to be set" do
39
+ object = klass.new
40
+ object.send("#{attribute}=", nil)
41
+ assert !object.valid?, "#{klass.name} does not require #{attribute}."
42
+ assert object.errors.on(attribute), "#{klass.name} does not require #{attribute}."
43
+ assert_contains(object.errors.on(attribute), message)
44
+ end
45
+ end
46
+ end
47
+
48
+ # Ensures that the model cannot be saved if one of the attributes listed is not unique.
49
+ # Requires an existing record
50
+ #
51
+ # Options:
52
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
53
+ # Regexp or string. Default = <tt>/taken/</tt>
54
+ # * <tt>:scoped_to</tt> - field(s) to scope the uniqueness to.
55
+ #
56
+ # Examples:
57
+ # should_require_unique_attributes :keyword, :username
58
+ # should_require_unique_attributes :name, :message => "O NOES! SOMEONE STOELED YER NAME!"
59
+ # should_require_unique_attributes :email, :scoped_to => :name
60
+ # should_require_unique_attributes :address, :scoped_to => [:first_name, :last_name]
61
+ #
62
+ def should_require_unique_attributes(*attributes)
63
+ message, scope = get_options!(attributes, :message, :scoped_to)
64
+ scope = [*scope].compact
65
+ message ||= /taken/
66
+
67
+ klass = model_class
68
+ attributes.each do |attribute|
69
+ attribute = attribute.to_sym
70
+ should "require unique value for #{attribute}#{" scoped to #{scope.join(', ')}" unless scope.blank?}" do
71
+ assert existing = klass.find(:first), "Can't find first #{klass}"
72
+ object = klass.new
73
+
74
+ object.send(:"#{attribute}=", existing.send(attribute))
75
+ if !scope.blank?
76
+ scope.each do |s|
77
+ assert_respond_to object, :"#{s}=", "#{klass.name} doesn't seem to have a #{s} attribute."
78
+ object.send(:"#{s}=", existing.send(s))
79
+ end
80
+ end
81
+
82
+ assert !object.valid?, "#{klass.name} does not require a unique value for #{attribute}."
83
+ assert object.errors.on(attribute), "#{klass.name} does not require a unique value for #{attribute}."
84
+
85
+ assert_contains(object.errors.on(attribute), message)
86
+
87
+ # Now test that the object is valid when changing the scoped attribute
88
+ # TODO: There is a chance that we could change the scoped field
89
+ # to a value that's already taken. An alternative implementation
90
+ # could actually find all values for scope and create a unique
91
+ # one.
92
+ if !scope.blank?
93
+ scope.each do |s|
94
+ # Assume the scope is a foreign key if the field is nil
95
+ object.send(:"#{s}=", existing.send(s).nil? ? 1 : existing.send(s).next)
96
+ end
97
+
98
+ object.errors.clear
99
+ object.valid?
100
+ scope.each do |s|
101
+ assert_does_not_contain(object.errors.on(attribute), message,
102
+ "after :#{s} set to #{object.send(s.to_sym)}")
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ # Ensures that the attribute cannot be set on mass update.
110
+ # Requires an existing record.
111
+ #
112
+ # should_protect_attributes :password, :admin_flag
113
+ #
114
+ def should_protect_attributes(*attributes)
115
+ get_options!(attributes)
116
+ klass = model_class
117
+
118
+ attributes.each do |attribute|
119
+ attribute = attribute.to_sym
120
+ should "protect #{attribute} from mass updates" do
121
+ protected = klass.protected_attributes || []
122
+ accessible = klass.accessible_attributes || []
123
+
124
+ assert protected.include?(attribute.to_s) || !accessible.include?(attribute.to_s),
125
+ (accessible.empty? ?
126
+ "#{klass} is protecting #{protected.to_a.to_sentence}, but not #{attribute}." :
127
+ "#{klass} has made #{attribute} accessible")
128
+ end
129
+ end
130
+ end
131
+
132
+ # Ensures that the attribute cannot be changed once the record has been created.
133
+ # Requires an existing record.
134
+ #
135
+ # should_have_readonly_attributes :password, :admin_flag
136
+ #
137
+ def should_have_readonly_attributes(*attributes)
138
+ get_options!(attributes)
139
+ klass = model_class
140
+
141
+ attributes.each do |attribute|
142
+ attribute = attribute.to_sym
143
+ should "make #{attribute} read-only" do
144
+ readonly = klass.readonly_attributes || []
145
+
146
+ assert readonly.include?(attribute.to_s),
147
+ (readonly.empty? ?
148
+ "#{klass} attribute #{attribute} is not read-only" :
149
+ "#{klass} is making #{readonly.to_a.to_sentence} read-only, but not #{attribute}.")
150
+ end
151
+ end
152
+ end
153
+
154
+ # Ensures that the attribute cannot be set to the given values
155
+ # Requires an existing record
156
+ #
157
+ # Options:
158
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
159
+ # Regexp or string. Default = <tt>/invalid/</tt>
160
+ #
161
+ # Example:
162
+ # should_not_allow_values_for :isbn, "bad 1", "bad 2"
163
+ #
164
+ def should_not_allow_values_for(attribute, *bad_values)
165
+ message = get_options!(bad_values, :message)
166
+ message ||= /invalid/
167
+ klass = model_class
168
+ bad_values.each do |v|
169
+ should "not allow #{attribute} to be set to #{v.inspect}" do
170
+ assert object = klass.find(:first), "Can't find first #{klass}"
171
+ object.send("#{attribute}=", v)
172
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
173
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
174
+ assert_contains(object.errors.on(attribute), message, "when set to \"#{v}\"")
175
+ end
176
+ end
177
+ end
178
+
179
+ # Ensures that the attribute can be set to the given values.
180
+ # Requires an existing record
181
+ #
182
+ # Example:
183
+ # should_allow_values_for :isbn, "isbn 1 2345 6789 0", "ISBN 1-2345-6789-0"
184
+ #
185
+ def should_allow_values_for(attribute, *good_values)
186
+ get_options!(good_values)
187
+ klass = model_class
188
+ good_values.each do |v|
189
+ should "allow #{attribute} to be set to #{v.inspect}" do
190
+ assert object = klass.find(:first), "Can't find first #{klass}"
191
+ object.send("#{attribute}=", v)
192
+ object.save
193
+ assert_nil object.errors.on(attribute)
194
+ end
195
+ end
196
+ end
197
+
198
+ # Ensures that the length of the attribute is in the given range
199
+ # Requires an existing record
200
+ #
201
+ # Options:
202
+ # * <tt>:short_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
203
+ # Regexp or string. Default = <tt>/short/</tt>
204
+ # * <tt>:long_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
205
+ # Regexp or string. Default = <tt>/long/</tt>
206
+ #
207
+ # Example:
208
+ # should_ensure_length_in_range :password, (6..20)
209
+ #
210
+ def should_ensure_length_in_range(attribute, range, opts = {})
211
+ short_message, long_message = get_options!([opts], :short_message, :long_message)
212
+ short_message ||= /short/
213
+ long_message ||= /long/
214
+
215
+ klass = model_class
216
+ min_length = range.first
217
+ max_length = range.last
218
+ same_length = (min_length == max_length)
219
+
220
+ if min_length > 0
221
+ should "not allow #{attribute} to be less than #{min_length} chars long" do
222
+ min_value = "x" * (min_length - 1)
223
+ assert object = klass.find(:first), "Can't find first #{klass}"
224
+ object.send("#{attribute}=", min_value)
225
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{min_value}\""
226
+ assert object.errors.on(attribute),
227
+ "There are no errors set on #{attribute} after being set to \"#{min_value}\""
228
+ assert_contains(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"")
229
+ end
230
+ end
231
+
232
+ if min_length > 0
233
+ should "allow #{attribute} to be exactly #{min_length} chars long" do
234
+ min_value = "x" * min_length
235
+ assert object = klass.find(:first), "Can't find first #{klass}"
236
+ object.send("#{attribute}=", min_value)
237
+ object.save
238
+ assert_does_not_contain(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"")
239
+ end
240
+ end
241
+
242
+ should "not allow #{attribute} to be more than #{max_length} chars long" do
243
+ max_value = "x" * (max_length + 1)
244
+ assert object = klass.find(:first), "Can't find first #{klass}"
245
+ object.send("#{attribute}=", max_value)
246
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{max_value}\""
247
+ assert object.errors.on(attribute),
248
+ "There are no errors set on #{attribute} after being set to \"#{max_value}\""
249
+ assert_contains(object.errors.on(attribute), long_message, "when set to \"#{max_value}\"")
250
+ end
251
+
252
+ unless same_length
253
+ should "allow #{attribute} to be exactly #{max_length} chars long" do
254
+ max_value = "x" * max_length
255
+ assert object = klass.find(:first), "Can't find first #{klass}"
256
+ object.send("#{attribute}=", max_value)
257
+ object.save
258
+ assert_does_not_contain(object.errors.on(attribute), long_message, "when set to \"#{max_value}\"")
259
+ end
260
+ end
261
+ end
262
+
263
+ # Ensures that the length of the attribute is at least a certain length
264
+ # Requires an existing record
265
+ #
266
+ # Options:
267
+ # * <tt>:short_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
268
+ # Regexp or string. Default = <tt>/short/</tt>
269
+ #
270
+ # Example:
271
+ # should_ensure_length_at_least :name, 3
272
+ #
273
+ def should_ensure_length_at_least(attribute, min_length, opts = {})
274
+ short_message = get_options!([opts], :short_message)
275
+ short_message ||= /short/
276
+
277
+ klass = model_class
278
+
279
+ if min_length > 0
280
+ min_value = "x" * (min_length - 1)
281
+ should "not allow #{attribute} to be less than #{min_length} chars long" do
282
+ assert object = klass.find(:first), "Can't find first #{klass}"
283
+ object.send("#{attribute}=", min_value)
284
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{min_value}\""
285
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{min_value}\""
286
+ assert_contains(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"")
287
+ end
288
+ end
289
+ should "allow #{attribute} to be at least #{min_length} chars long" do
290
+ valid_value = "x" * (min_length)
291
+ assert object = klass.find(:first), "Can't find first #{klass}"
292
+ object.send("#{attribute}=", valid_value)
293
+ assert object.save, "Could not save #{klass} with #{attribute} set to \"#{valid_value}\""
294
+ end
295
+ end
296
+
297
+ # Ensures that the length of the attribute is exactly a certain length
298
+ # Requires an existing record
299
+ #
300
+ # Options:
301
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
302
+ # Regexp or string. Default = <tt>/short/</tt>
303
+ #
304
+ # Example:
305
+ # should_ensure_length_is :ssn, 9
306
+ #
307
+ def should_ensure_length_is(attribute, length, opts = {})
308
+ message = get_options!([opts], :message)
309
+ message ||= /wrong length/
310
+
311
+ klass = model_class
312
+
313
+ should "not allow #{attribute} to be less than #{length} chars long" do
314
+ min_value = "x" * (length - 1)
315
+ assert object = klass.find(:first), "Can't find first #{klass}"
316
+ object.send("#{attribute}=", min_value)
317
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{min_value}\""
318
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{min_value}\""
319
+ assert_contains(object.errors.on(attribute), message, "when set to \"#{min_value}\"")
320
+ end
321
+
322
+ should "not allow #{attribute} to be greater than #{length} chars long" do
323
+ max_value = "x" * (length + 1)
324
+ assert object = klass.find(:first), "Can't find first #{klass}"
325
+ object.send("#{attribute}=", max_value)
326
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{max_value}\""
327
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{max_value}\""
328
+ assert_contains(object.errors.on(attribute), message, "when set to \"#{max_value}\"")
329
+ end
330
+
331
+ should "allow #{attribute} to be #{length} chars long" do
332
+ valid_value = "x" * (length)
333
+ assert object = klass.find(:first), "Can't find first #{klass}"
334
+ object.send("#{attribute}=", valid_value)
335
+ object.save
336
+ assert_does_not_contain(object.errors.on(attribute), message, "when set to \"#{valid_value}\"")
337
+ end
338
+
339
+ end
340
+
341
+ # Ensure that the attribute is in the range specified
342
+ # Requires an existing record
343
+ #
344
+ # Options:
345
+ # * <tt>:low_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
346
+ # Regexp or string. Default = <tt>/included/</tt>
347
+ # * <tt>:high_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
348
+ # Regexp or string. Default = <tt>/included/</tt>
349
+ #
350
+ # Example:
351
+ # should_ensure_value_in_range :age, (0..100)
352
+ #
353
+ def should_ensure_value_in_range(attribute, range, opts = {})
354
+ low_message, high_message = get_options!([opts], :low_message, :high_message)
355
+ low_message ||= /included/
356
+ high_message ||= /included/
357
+
358
+ klass = model_class
359
+ min = range.first
360
+ max = range.last
361
+
362
+ should "not allow #{attribute} to be less than #{min}" do
363
+ v = min - 1
364
+ assert object = klass.find(:first), "Can't find first #{klass}"
365
+ object.send("#{attribute}=", v)
366
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
367
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
368
+ assert_contains(object.errors.on(attribute), low_message, "when set to \"#{v}\"")
369
+ end
370
+
371
+ should "allow #{attribute} to be #{min}" do
372
+ v = min
373
+ assert object = klass.find(:first), "Can't find first #{klass}"
374
+ object.send("#{attribute}=", v)
375
+ object.save
376
+ assert_does_not_contain(object.errors.on(attribute), low_message, "when set to \"#{v}\"")
377
+ end
378
+
379
+ should "not allow #{attribute} to be more than #{max}" do
380
+ v = max + 1
381
+ assert object = klass.find(:first), "Can't find first #{klass}"
382
+ object.send("#{attribute}=", v)
383
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
384
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
385
+ assert_contains(object.errors.on(attribute), high_message, "when set to \"#{v}\"")
386
+ end
387
+
388
+ should "allow #{attribute} to be #{max}" do
389
+ v = max
390
+ assert object = klass.find(:first), "Can't find first #{klass}"
391
+ object.send("#{attribute}=", v)
392
+ object.save
393
+ assert_does_not_contain(object.errors.on(attribute), high_message, "when set to \"#{v}\"")
394
+ end
395
+ end
396
+
397
+ # Ensure that the attribute is numeric
398
+ # Requires an existing record
399
+ #
400
+ # Options:
401
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
402
+ # Regexp or string. Default = <tt>/number/</tt>
403
+ #
404
+ # Example:
405
+ # should_only_allow_numeric_values_for :age
406
+ #
407
+ def should_only_allow_numeric_values_for(*attributes)
408
+ message = get_options!(attributes, :message)
409
+ message ||= /number/
410
+ klass = model_class
411
+ attributes.each do |attribute|
412
+ attribute = attribute.to_sym
413
+ should "only allow numeric values for #{attribute}" do
414
+ assert object = klass.find(:first), "Can't find first #{klass}"
415
+ object.send(:"#{attribute}=", "abcd")
416
+ assert !object.valid?, "Instance is still valid"
417
+ assert_contains(object.errors.on(attribute), message)
418
+ end
419
+ end
420
+ end
421
+
422
+ # Ensures that the has_many relationship exists. Will also test that the
423
+ # associated table has the required columns. Works with polymorphic
424
+ # associations.
425
+ #
426
+ # Options:
427
+ # * <tt>:through</tt> - association name for <tt>has_many :through</tt>
428
+ # * <tt>:dependent</tt> - tests that the association makes use of the dependent option.
429
+ #
430
+ # Example:
431
+ # should_have_many :friends
432
+ # should_have_many :enemies, :through => :friends
433
+ # should_have_many :enemies, :dependent => :destroy
434
+ #
435
+ def should_have_many(*associations)
436
+ through, dependent = get_options!(associations, :through, :dependent)
437
+ klass = model_class
438
+ associations.each do |association|
439
+ name = "have many #{association}"
440
+ name += " through #{through}" if through
441
+ name += " dependent => #{dependent}" if dependent
442
+ should name do
443
+ reflection = klass.reflect_on_association(association)
444
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
445
+ assert_equal :has_many, reflection.macro
446
+
447
+ associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize
448
+
449
+ if through
450
+ through_reflection = klass.reflect_on_association(through)
451
+ assert through_reflection, "#{klass.name} does not have any relationship to #{through}"
452
+ assert_equal(through, reflection.options[:through])
453
+ end
454
+
455
+ if dependent
456
+ assert_equal dependent.to_s,
457
+ reflection.options[:dependent].to_s,
458
+ "#{associated_klass.name} should have #{dependent} dependency"
459
+ end
460
+
461
+ # Check for the existence of the foreign key on the other table
462
+ unless reflection.options[:through]
463
+ if reflection.options[:foreign_key]
464
+ fk = reflection.options[:foreign_key]
465
+ elsif reflection.options[:as]
466
+ fk = reflection.options[:as].to_s.foreign_key
467
+ else
468
+ fk = reflection.primary_key_name
469
+ end
470
+
471
+ assert associated_klass.column_names.include?(fk.to_s),
472
+ "#{associated_klass.name} does not have a #{fk} foreign key."
473
+ end
474
+ end
475
+ end
476
+ end
477
+
478
+ # Ensure that the has_one relationship exists. Will also test that the
479
+ # associated table has the required columns. Works with polymorphic
480
+ # associations.
481
+ #
482
+ # Example:
483
+ # should_have_one :god # unless hindu
484
+ #
485
+ def should_have_one(*associations)
486
+ get_options!(associations)
487
+ klass = model_class
488
+ associations.each do |association|
489
+ should "have one #{association}" do
490
+ reflection = klass.reflect_on_association(association)
491
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
492
+ assert_equal :has_one, reflection.macro
493
+
494
+ associated_klass = (reflection.options[:class_name] || association.to_s.camelize).constantize
495
+
496
+ if reflection.options[:foreign_key]
497
+ fk = reflection.options[:foreign_key]
498
+ elsif reflection.options[:as]
499
+ fk = reflection.options[:as].to_s.foreign_key
500
+ fk_type = fk.gsub(/_id$/, '_type')
501
+ assert associated_klass.column_names.include?(fk_type),
502
+ "#{associated_klass.name} does not have a #{fk_type} column."
503
+ else
504
+ fk = klass.name.foreign_key
505
+ end
506
+ assert associated_klass.column_names.include?(fk.to_s),
507
+ "#{associated_klass.name} does not have a #{fk} foreign key."
508
+ end
509
+ end
510
+ end
511
+
512
+ # Ensures that the has_and_belongs_to_many relationship exists, and that the join
513
+ # table is in place.
514
+ #
515
+ # should_have_and_belong_to_many :posts, :cars
516
+ #
517
+ def should_have_and_belong_to_many(*associations)
518
+ get_options!(associations)
519
+ klass = model_class
520
+
521
+ associations.each do |association|
522
+ should "should have and belong to many #{association}" do
523
+ reflection = klass.reflect_on_association(association)
524
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
525
+ assert_equal :has_and_belongs_to_many, reflection.macro
526
+ table = reflection.options[:join_table]
527
+ assert ::ActiveRecord::Base.connection.tables.include?(table), "table #{table} doesn't exist"
528
+ end
529
+ end
530
+ end
531
+
532
+ # Ensure that the belongs_to relationship exists.
533
+ #
534
+ # should_belong_to :parent
535
+ #
536
+ def should_belong_to(*associations)
537
+ get_options!(associations)
538
+ klass = model_class
539
+ associations.each do |association|
540
+ should "belong_to #{association}" do
541
+ reflection = klass.reflect_on_association(association)
542
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
543
+ assert_equal :belongs_to, reflection.macro
544
+
545
+ unless reflection.options[:polymorphic]
546
+ associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize
547
+ fk = reflection.options[:foreign_key] || reflection.primary_key_name
548
+ assert klass.column_names.include?(fk.to_s), "#{klass.name} does not have a #{fk} foreign key."
549
+ end
550
+ end
551
+ end
552
+ end
553
+
554
+ # Ensure that the given class methods are defined on the model.
555
+ #
556
+ # should_have_class_methods :find, :destroy
557
+ #
558
+ def should_have_class_methods(*methods)
559
+ get_options!(methods)
560
+ klass = model_class
561
+ methods.each do |method|
562
+ should "respond to class method ##{method}" do
563
+ assert_respond_to klass, method, "#{klass.name} does not have class method #{method}"
564
+ end
565
+ end
566
+ end
567
+
568
+ # Ensure that the given instance methods are defined on the model.
569
+ #
570
+ # should_have_instance_methods :email, :name, :name=
571
+ #
572
+ def should_have_instance_methods(*methods)
573
+ get_options!(methods)
574
+ klass = model_class
575
+ methods.each do |method|
576
+ should "respond to instance method ##{method}" do
577
+ assert_respond_to klass.new, method, "#{klass.name} does not have instance method #{method}"
578
+ end
579
+ end
580
+ end
581
+
582
+ # Ensure that the given columns are defined on the models backing SQL table.
583
+ #
584
+ # should_have_db_columns :id, :email, :name, :created_at
585
+ #
586
+ def should_have_db_columns(*columns)
587
+ column_type = get_options!(columns, :type)
588
+ klass = model_class
589
+ columns.each do |name|
590
+ test_name = "have column #{name}"
591
+ test_name += " of type #{column_type}" if column_type
592
+ should test_name do
593
+ column = klass.columns.detect {|c| c.name == name.to_s }
594
+ assert column, "#{klass.name} does not have column #{name}"
595
+ end
596
+ end
597
+ end
598
+
599
+ # Ensure that the given column is defined on the models backing SQL table. The options are the same as
600
+ # the instance variables defined on the column definition: :precision, :limit, :default, :null,
601
+ # :primary, :type, :scale, and :sql_type.
602
+ #
603
+ # should_have_db_column :email, :type => "string", :default => nil, :precision => nil, :limit => 255,
604
+ # :null => true, :primary => false, :scale => nil, :sql_type => 'varchar(255)'
605
+ #
606
+ def should_have_db_column(name, opts = {})
607
+ klass = model_class
608
+ test_name = "have column named :#{name}"
609
+ test_name += " with options " + opts.inspect unless opts.empty?
610
+ should test_name do
611
+ column = klass.columns.detect {|c| c.name == name.to_s }
612
+ assert column, "#{klass.name} does not have column #{name}"
613
+ opts.each do |k, v|
614
+ assert_equal column.instance_variable_get("@#{k}").to_s, v.to_s, ":#{name} column on table for #{klass} does not match option :#{k}"
615
+ end
616
+ end
617
+ end
618
+
619
+ # Ensures that there are DB indices on the given columns or tuples of columns.
620
+ # Also aliased to should_have_index for readability
621
+ #
622
+ # should_have_indices :email, :name, [:commentable_type, :commentable_id]
623
+ # should_have_index :age
624
+ #
625
+ def should_have_indices(*columns)
626
+ table = model_class.name.tableize
627
+ indices = ::ActiveRecord::Base.connection.indexes(table).map(&:columns)
628
+
629
+ columns.each do |column|
630
+ should "have index on #{table} for #{column.inspect}" do
631
+ columns = [column].flatten.map(&:to_s)
632
+ assert_contains(indices, columns)
633
+ end
634
+ end
635
+ end
636
+
637
+ alias_method :should_have_index, :should_have_indices
638
+
639
+ # Ensures that the model cannot be saved if one of the attributes listed is not accepted.
640
+ #
641
+ # Options:
642
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
643
+ # Regexp or string. Default = <tt>/must be accepted/</tt>
644
+ #
645
+ # Example:
646
+ # should_require_acceptance_of :eula
647
+ #
648
+ def should_require_acceptance_of(*attributes)
649
+ message = get_options!(attributes, :message)
650
+ message ||= /must be accepted/
651
+ klass = model_class
652
+
653
+ attributes.each do |attribute|
654
+ should "require #{attribute} to be accepted" do
655
+ object = klass.new
656
+ object.send("#{attribute}=", false)
657
+
658
+ assert !object.valid?, "#{klass.name} does not require acceptance of #{attribute}."
659
+ assert object.errors.on(attribute), "#{klass.name} does not require acceptance of #{attribute}."
660
+ assert_contains(object.errors.on(attribute), message)
661
+ end
662
+ end
663
+ end
664
+
665
+ private
666
+
667
+ include ThoughtBot::Shoulda::Private
668
+ end
669
+ end
670
+ end