shoulda-matchers 1.0.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CONTRIBUTION_GUIDELINES.rdoc +10 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +116 -0
- data/MIT-LICENSE +22 -0
- data/README.rdoc +70 -0
- data/Rakefile +50 -0
- data/lib/shoulda-matchers.rb +8 -0
- data/lib/shoulda/matchers/action_controller.rb +38 -0
- data/lib/shoulda/matchers/action_controller/assign_to_matcher.rb +114 -0
- data/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +50 -0
- data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +62 -0
- data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +54 -0
- data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +99 -0
- data/lib/shoulda/matchers/action_controller/respond_with_content_type_matcher.rb +74 -0
- data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +85 -0
- data/lib/shoulda/matchers/action_controller/route_matcher.rb +93 -0
- data/lib/shoulda/matchers/action_controller/set_session_matcher.rb +98 -0
- data/lib/shoulda/matchers/action_controller/set_the_flash_matcher.rb +94 -0
- data/lib/shoulda/matchers/action_mailer.rb +22 -0
- data/lib/shoulda/matchers/action_mailer/have_sent_email.rb +115 -0
- data/lib/shoulda/matchers/active_record.rb +42 -0
- data/lib/shoulda/matchers/active_record/allow_mass_assignment_of_matcher.rb +83 -0
- data/lib/shoulda/matchers/active_record/allow_value_matcher.rb +110 -0
- data/lib/shoulda/matchers/active_record/association_matcher.rb +226 -0
- data/lib/shoulda/matchers/active_record/ensure_inclusion_of_matcher.rb +87 -0
- data/lib/shoulda/matchers/active_record/ensure_length_of_matcher.rb +141 -0
- data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +169 -0
- data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +112 -0
- data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +59 -0
- data/lib/shoulda/matchers/active_record/helpers.rb +34 -0
- data/lib/shoulda/matchers/active_record/validate_acceptance_of_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_record/validate_format_of_matcher.rb +65 -0
- data/lib/shoulda/matchers/active_record/validate_numericality_of_matcher.rb +39 -0
- data/lib/shoulda/matchers/active_record/validate_presence_of_matcher.rb +60 -0
- data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +148 -0
- data/lib/shoulda/matchers/active_record/validation_matcher.rb +56 -0
- data/lib/shoulda/matchers/integrations/rspec.rb +23 -0
- data/lib/shoulda/matchers/integrations/test_unit.rb +41 -0
- data/lib/shoulda/matchers/version.rb +5 -0
- 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
|