shoulda-matchers 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/CONTRIBUTION_GUIDELINES.rdoc +10 -0
  2. data/Gemfile +10 -0
  3. data/Gemfile.lock +116 -0
  4. data/MIT-LICENSE +22 -0
  5. data/README.rdoc +70 -0
  6. data/Rakefile +50 -0
  7. data/lib/shoulda-matchers.rb +8 -0
  8. data/lib/shoulda/matchers/action_controller.rb +38 -0
  9. data/lib/shoulda/matchers/action_controller/assign_to_matcher.rb +114 -0
  10. data/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +50 -0
  11. data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +62 -0
  12. data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +54 -0
  13. data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +99 -0
  14. data/lib/shoulda/matchers/action_controller/respond_with_content_type_matcher.rb +74 -0
  15. data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +85 -0
  16. data/lib/shoulda/matchers/action_controller/route_matcher.rb +93 -0
  17. data/lib/shoulda/matchers/action_controller/set_session_matcher.rb +98 -0
  18. data/lib/shoulda/matchers/action_controller/set_the_flash_matcher.rb +94 -0
  19. data/lib/shoulda/matchers/action_mailer.rb +22 -0
  20. data/lib/shoulda/matchers/action_mailer/have_sent_email.rb +115 -0
  21. data/lib/shoulda/matchers/active_record.rb +42 -0
  22. data/lib/shoulda/matchers/active_record/allow_mass_assignment_of_matcher.rb +83 -0
  23. data/lib/shoulda/matchers/active_record/allow_value_matcher.rb +110 -0
  24. data/lib/shoulda/matchers/active_record/association_matcher.rb +226 -0
  25. data/lib/shoulda/matchers/active_record/ensure_inclusion_of_matcher.rb +87 -0
  26. data/lib/shoulda/matchers/active_record/ensure_length_of_matcher.rb +141 -0
  27. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +169 -0
  28. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +112 -0
  29. data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +59 -0
  30. data/lib/shoulda/matchers/active_record/helpers.rb +34 -0
  31. data/lib/shoulda/matchers/active_record/validate_acceptance_of_matcher.rb +41 -0
  32. data/lib/shoulda/matchers/active_record/validate_format_of_matcher.rb +65 -0
  33. data/lib/shoulda/matchers/active_record/validate_numericality_of_matcher.rb +39 -0
  34. data/lib/shoulda/matchers/active_record/validate_presence_of_matcher.rb +60 -0
  35. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +148 -0
  36. data/lib/shoulda/matchers/active_record/validation_matcher.rb +56 -0
  37. data/lib/shoulda/matchers/integrations/rspec.rb +23 -0
  38. data/lib/shoulda/matchers/integrations/test_unit.rb +41 -0
  39. data/lib/shoulda/matchers/version.rb +5 -0
  40. metadata +113 -0
@@ -0,0 +1,169 @@
1
+ module Shoulda # :nodoc:
2
+ module Matchers
3
+ module ActiveRecord # :nodoc:
4
+
5
+ # Ensures the database column exists.
6
+ #
7
+ # Options:
8
+ # * <tt>of_type</tt> - db column type (:integer, :string, etc.)
9
+ # * <tt>with_options</tt> - same options available in migrations
10
+ # (:default, :null, :limit, :precision, :scale)
11
+ #
12
+ # Examples:
13
+ # it { should_not have_db_column(:admin).of_type(:boolean) }
14
+ # it { should have_db_column(:salary).
15
+ # of_type(:decimal).
16
+ # with_options(:precision => 10, :scale => 2) }
17
+ #
18
+ def have_db_column(column)
19
+ HaveDbColumnMatcher.new(:have_db_column, column)
20
+ end
21
+
22
+ class HaveDbColumnMatcher # :nodoc:
23
+ def initialize(macro, column)
24
+ @macro = macro
25
+ @column = column
26
+ end
27
+
28
+ def of_type(column_type)
29
+ @column_type = column_type
30
+ self
31
+ end
32
+
33
+ def with_options(opts = {})
34
+ @precision = opts[:precision]
35
+ @limit = opts[:limit]
36
+ @default = opts[:default]
37
+ @null = opts[:null]
38
+ @scale = opts[:scale]
39
+ self
40
+ end
41
+
42
+ def matches?(subject)
43
+ @subject = subject
44
+ column_exists? &&
45
+ correct_column_type? &&
46
+ correct_precision? &&
47
+ correct_limit? &&
48
+ correct_default? &&
49
+ correct_null? &&
50
+ correct_scale?
51
+ end
52
+
53
+ def failure_message
54
+ "Expected #{expectation} (#{@missing})"
55
+ end
56
+
57
+ def negative_failure_message
58
+ "Did not expect #{expectation}"
59
+ end
60
+
61
+ def description
62
+ desc = "have db column named #{@column}"
63
+ desc << " of type #{@column_type}" unless @column_type.nil?
64
+ desc << " of precision #{@precision}" unless @precision.nil?
65
+ desc << " of limit #{@limit}" unless @limit.nil?
66
+ desc << " of default #{@default}" unless @default.nil?
67
+ desc << " of null #{@null}" unless @null.nil?
68
+ desc << " of primary #{@primary}" unless @primary.nil?
69
+ desc << " of scale #{@scale}" unless @scale.nil?
70
+ desc
71
+ end
72
+
73
+ protected
74
+
75
+ def column_exists?
76
+ if model_class.column_names.include?(@column.to_s)
77
+ true
78
+ else
79
+ @missing = "#{model_class} does not have a db column named #{@column}."
80
+ false
81
+ end
82
+ end
83
+
84
+ def correct_column_type?
85
+ return true if @column_type.nil?
86
+ if matched_column.type.to_s == @column_type.to_s
87
+ true
88
+ else
89
+ @missing = "#{model_class} has a db column named #{@column} " <<
90
+ "of type #{matched_column.type}, not #{@column_type}."
91
+ false
92
+ end
93
+ end
94
+
95
+ def correct_precision?
96
+ return true if @precision.nil?
97
+ if matched_column.precision.to_s == @precision.to_s
98
+ true
99
+ else
100
+ @missing = "#{model_class} has a db column named #{@column} " <<
101
+ "of precision #{matched_column.precision}, " <<
102
+ "not #{@precision}."
103
+ false
104
+ end
105
+ end
106
+
107
+ def correct_limit?
108
+ return true if @limit.nil?
109
+ if matched_column.limit.to_s == @limit.to_s
110
+ true
111
+ else
112
+ @missing = "#{model_class} has a db column named #{@column} " <<
113
+ "of limit #{matched_column.limit}, " <<
114
+ "not #{@limit}."
115
+ false
116
+ end
117
+ end
118
+
119
+ def correct_default?
120
+ return true if @default.nil?
121
+ if matched_column.default.to_s == @default.to_s
122
+ true
123
+ else
124
+ @missing = "#{model_class} has a db column named #{@column} " <<
125
+ "of default #{matched_column.default}, " <<
126
+ "not #{@default}."
127
+ false
128
+ end
129
+ end
130
+
131
+ def correct_null?
132
+ return true if @null.nil?
133
+ if matched_column.null.to_s == @null.to_s
134
+ true
135
+ else
136
+ @missing = "#{model_class} has a db column named #{@column} " <<
137
+ "of null #{matched_column.null}, " <<
138
+ "not #{@null}."
139
+ false
140
+ end
141
+ end
142
+
143
+ def correct_scale?
144
+ return true if @scale.nil?
145
+ if matched_column.scale.to_s == @scale.to_s
146
+ true
147
+ else
148
+ @missing = "#{model_class} has a db column named #{@column} " <<
149
+ "of scale #{matched_column.scale}, not #{@scale}."
150
+ false
151
+ end
152
+ end
153
+
154
+ def matched_column
155
+ model_class.columns.detect { |each| each.name == @column.to_s }
156
+ end
157
+
158
+ def model_class
159
+ @subject.class
160
+ end
161
+
162
+ def expectation
163
+ expected = "#{model_class.name} to #{description}"
164
+ end
165
+ end
166
+
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,112 @@
1
+ module Shoulda # :nodoc:
2
+ module Matchers
3
+ module ActiveRecord # :nodoc:
4
+
5
+ # Ensures that there are DB indices on the given columns or tuples of
6
+ # columns.
7
+ #
8
+ # Options:
9
+ # * <tt>unique</tt> - whether or not the index has a unique
10
+ # constraint. Use <tt>true</tt> to explicitly test for a unique
11
+ # constraint. Use <tt>false</tt> to explicitly test for a non-unique
12
+ # constraint. Use <tt>nil</tt> if you don't care whether the index is
13
+ # unique or not. Default = <tt>nil</tt>
14
+ #
15
+ # Examples:
16
+ #
17
+ # it { should have_db_index(:age) }
18
+ # it { should have_db_index([:commentable_type, :commentable_id]) }
19
+ # it { should have_db_index(:ssn).unique(true) }
20
+ #
21
+ def have_db_index(columns)
22
+ HaveDbIndexMatcher.new(:have_index, columns)
23
+ end
24
+
25
+ class HaveDbIndexMatcher # :nodoc:
26
+ def initialize(macro, columns)
27
+ @macro = macro
28
+ @columns = normalize_columns_to_array(columns)
29
+ end
30
+
31
+ def unique(unique)
32
+ @unique = unique
33
+ self
34
+ end
35
+
36
+ def matches?(subject)
37
+ @subject = subject
38
+ index_exists? && correct_unique?
39
+ end
40
+
41
+ def failure_message
42
+ "Expected #{expectation} (#{@missing})"
43
+ end
44
+
45
+ def negative_failure_message
46
+ "Did not expect #{expectation}"
47
+ end
48
+
49
+ def description
50
+ "have a #{index_type} index on columns #{@columns.join(' and ')}"
51
+ end
52
+
53
+ protected
54
+
55
+ def index_exists?
56
+ ! matched_index.nil?
57
+ end
58
+
59
+ def correct_unique?
60
+ return true if @unique.nil?
61
+ if matched_index.unique == @unique
62
+ true
63
+ else
64
+ @missing = "#{table_name} has an index named #{matched_index.name} " <<
65
+ "of unique #{matched_index.unique}, not #{@unique}."
66
+ false
67
+ end
68
+ end
69
+
70
+ def matched_index
71
+ indexes.detect { |each| each.columns == @columns }
72
+ end
73
+
74
+ def model_class
75
+ @subject.class
76
+ end
77
+
78
+ def table_name
79
+ model_class.table_name
80
+ end
81
+
82
+ def indexes
83
+ ::ActiveRecord::Base.connection.indexes(table_name)
84
+ end
85
+
86
+ def expectation
87
+ expected = "#{model_class.name} to #{description}"
88
+ end
89
+
90
+ def index_type
91
+ case @unique
92
+ when nil
93
+ ''
94
+ when false
95
+ 'non-unique'
96
+ else
97
+ 'unique'
98
+ end
99
+ end
100
+
101
+ def normalize_columns_to_array(columns)
102
+ if columns.class == Array
103
+ columns.collect { |each| each.to_s }
104
+ else
105
+ [columns.to_s]
106
+ end
107
+ end
108
+ end
109
+
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,59 @@
1
+ module Shoulda # :nodoc:
2
+ module Matchers
3
+ module ActiveRecord # :nodoc:
4
+
5
+ # Ensures that the attribute cannot be changed once the record has been
6
+ # created.
7
+ #
8
+ # it { should have_readonly_attributes(:password) }
9
+ #
10
+ def have_readonly_attribute(value)
11
+ HaveReadonlyAttributeMatcher.new(value)
12
+ end
13
+
14
+ class HaveReadonlyAttributeMatcher # :nodoc:
15
+
16
+ def initialize(attribute)
17
+ @attribute = attribute.to_s
18
+ end
19
+
20
+ def matches?(subject)
21
+ @subject = subject
22
+ if readonly_attributes.include?(@attribute)
23
+ @negative_failure_message =
24
+ "Did not expect #{@attribute} to be read-only"
25
+ true
26
+ else
27
+ if readonly_attributes.empty?
28
+ @failure_message = "#{class_name} attribute #{@attribute} " <<
29
+ "is not read-only"
30
+ else
31
+ @failure_message = "#{class_name} is making " <<
32
+ "#{readonly_attributes.to_sentence} " <<
33
+ "read-only, but not #{@attribute}."
34
+ end
35
+ false
36
+ end
37
+ end
38
+
39
+ attr_reader :failure_message, :negative_failure_message
40
+
41
+ def description
42
+ "make #{@attribute} read-only"
43
+ end
44
+
45
+ private
46
+
47
+ def readonly_attributes
48
+ @readonly_attributes ||= (@subject.class.readonly_attributes || [])
49
+ end
50
+
51
+ def class_name
52
+ @subject.class.name
53
+ end
54
+
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,34 @@
1
+ module Shoulda # :nodoc:
2
+ module Matchers
3
+ module ActiveRecord # :nodoc:
4
+ module Helpers
5
+ def pretty_error_messages(obj) # :nodoc:
6
+ obj.errors.map do |a, m|
7
+ msg = "#{a} #{m}"
8
+ msg << " (#{obj.send(a).inspect})" unless a.to_sym == :base
9
+ end
10
+ end
11
+
12
+ # Helper method that determines the default error message used by Active
13
+ # Record. Works for both existing Rails 2.1 and Rails 2.2 with the newly
14
+ # introduced I18n module used for localization.
15
+ #
16
+ # default_error_message(:blank)
17
+ # default_error_message(:too_short, :count => 5)
18
+ # default_error_message(:too_long, :count => 60)
19
+ def default_error_message(key, values = {})
20
+ if Object.const_defined?(:I18n) # Rails >= 2.2
21
+ result = I18n.translate("activerecord.errors.messages.#{key}", values)
22
+ if result =~ /^translation missing/
23
+ I18n.translate("errors.messages.#{key}", values)
24
+ else
25
+ result
26
+ end
27
+ else # Rails <= 2.1.x
28
+ ::ActiveRecord::Errors.default_error_messages[key] % values[:count]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ module Shoulda # :nodoc:
2
+ module Matchers
3
+ module ActiveRecord # :nodoc:
4
+
5
+ # Ensures that the model cannot be saved the given attribute is not
6
+ # accepted.
7
+ #
8
+ # Options:
9
+ # * <tt>with_message</tt> - value the test expects to find in
10
+ # <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
11
+ # translation for <tt>:accepted</tt>.
12
+ #
13
+ # Example:
14
+ # it { should validate_acceptance_of(:eula) }
15
+ #
16
+ def validate_acceptance_of(attr)
17
+ ValidateAcceptanceOfMatcher.new(attr)
18
+ end
19
+
20
+ class ValidateAcceptanceOfMatcher < ValidationMatcher # :nodoc:
21
+
22
+ def with_message(message)
23
+ @expected_message = message if message
24
+ self
25
+ end
26
+
27
+ def matches?(subject)
28
+ super(subject)
29
+ @expected_message ||= :accepted
30
+ disallows_value_of(false, @expected_message)
31
+ end
32
+
33
+ def description
34
+ "require #{@attribute} to be accepted"
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,65 @@
1
+ module Shoulda # :nodoc:
2
+ module Matchers
3
+ module ActiveRecord # :nodoc:
4
+
5
+ # Ensures that the model is not valid if the given attribute is not
6
+ # formatted correctly.
7
+ #
8
+ # Options:
9
+ # * <tt>with_message</tt> - value the test expects to find in
10
+ # <tt>errors.on(:attribute)</tt>. <tt>Regexp</tt> or <tt>String</tt>.
11
+ # Defaults to the translation for <tt>:blank</tt>.
12
+ # * <tt>with(string to test against)</tt>
13
+ # * <tt>not_with(string to test against)</tt>
14
+ #
15
+ # Examples:
16
+ # it { should validate_format_of(:name).
17
+ # with('12345').
18
+ # with_message(/is not optional/) }
19
+ # it { should validate_format_of(:name).
20
+ # not_with('12D45').
21
+ # with_message(/is not optional/) }
22
+ #
23
+ def validate_format_of(attr)
24
+ ValidateFormatOfMatcher.new(attr)
25
+ end
26
+
27
+ class ValidateFormatOfMatcher < ValidationMatcher # :nodoc:
28
+
29
+ def initialize(attribute)
30
+ super
31
+ end
32
+
33
+ def with_message(message)
34
+ @expected_message = message if message
35
+ self
36
+ end
37
+
38
+ def with(value)
39
+ raise "You may not call both with and not_with" if @value_to_fail
40
+ @value_to_pass = value
41
+ self
42
+ end
43
+
44
+ def not_with(value)
45
+ raise "You may not call both with and not_with" if @value_to_pass
46
+ @value_to_fail = value
47
+ self
48
+ end
49
+
50
+ def matches?(subject)
51
+ super(subject)
52
+ @expected_message ||= :blank
53
+ return disallows_value_of(@value_to_fail, @expected_message) if @value_to_fail
54
+ allows_value_of(@value_to_pass, @expected_message) if @value_to_pass
55
+ end
56
+
57
+ def description
58
+ "#{@attribute} have a valid format"
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+ end
65
+ end