shoulda-matchers 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|