mirror of
https://github.com/thoughtbot/shoulda-matchers.git
synced 2022-11-09 12:01:38 -05:00
Add without_scopes
to enum matcher. (#1453)
Rails 6 added the `_scopes: false` option when defining enums. This will prevent rails from creating class scopes for each of the values. Previously, shoulda-matchers checked for the presence of singleton methods as a way to check that the corresponding enum methods have been defined. In rails 6, if someone were using `_scopes: false` the matcher will fail. Instead, we should check on the presence of the instance methods that are generated: `#{value}!` and `#{value}?` as the default checker for the enum values generating methods. We can then check on the scopes/singleton methods generated when using the new `without_scopes` matcher.
This commit is contained in:
parent
14040194ad
commit
38db235515
2 changed files with 207 additions and 26 deletions
|
@ -116,7 +116,7 @@ module Shoulda
|
|||
## ##### with_prefix
|
||||
#
|
||||
# Use `with_prefix` to test that the enum is defined with a `_prefix`
|
||||
# option (Rails 5 only). Can take either a boolean or a symbol:
|
||||
# option (Rails 6+ only). Can take either a boolean or a symbol:
|
||||
#
|
||||
# class Issue < ActiveRecord::Base
|
||||
# enum status: [:open, :closed], _prefix: :old
|
||||
|
@ -163,6 +163,30 @@ module Shoulda
|
|||
# with_suffix
|
||||
# end
|
||||
#
|
||||
# ##### without_scopes
|
||||
#
|
||||
# Use `without_scopes` to test that the enum is defined with
|
||||
# '_scopes: false' option (Rails 5 only). Can take either a boolean or a
|
||||
# symbol:
|
||||
#
|
||||
# class Issue < ActiveRecord::Base
|
||||
# enum status: [:open, :closed], _scopes: false
|
||||
# end
|
||||
#
|
||||
# # RSpec
|
||||
# RSpec.describe Issue, type: :model do
|
||||
# it do
|
||||
# should define_enum_for(:status).
|
||||
# without_scopes
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # Minitest (Shoulda)
|
||||
# class ProcessTest < ActiveSupport::TestCase
|
||||
# should define_enum_for(:status).
|
||||
# without_scopes
|
||||
# end
|
||||
#
|
||||
# @return [DefineEnumForMatcher]
|
||||
#
|
||||
def define_enum_for(attribute_name)
|
||||
|
@ -173,7 +197,7 @@ module Shoulda
|
|||
class DefineEnumForMatcher
|
||||
def initialize(attribute_name)
|
||||
@attribute_name = attribute_name
|
||||
@options = { expected_enum_values: [] }
|
||||
@options = { expected_enum_values: [], scopes: true }
|
||||
end
|
||||
|
||||
def description
|
||||
|
@ -226,13 +250,19 @@ module Shoulda
|
|||
self
|
||||
end
|
||||
|
||||
def without_scopes
|
||||
options[:scopes] = false
|
||||
self
|
||||
end
|
||||
|
||||
def matches?(subject)
|
||||
@record = subject
|
||||
|
||||
enum_defined? &&
|
||||
enum_values_match? &&
|
||||
column_type_matches? &&
|
||||
enum_value_methods_exist?
|
||||
enum_value_methods_exist? &&
|
||||
scope_presence_matches?
|
||||
end
|
||||
|
||||
def failure_message
|
||||
|
@ -294,6 +324,10 @@ module Shoulda
|
|||
expectation << "_#{expected_suffix}".inspect
|
||||
end
|
||||
|
||||
if exclude_scopes?
|
||||
expectation << ' with no scopes'
|
||||
end
|
||||
|
||||
expectation
|
||||
else
|
||||
simple_description
|
||||
|
@ -387,28 +421,10 @@ module Shoulda
|
|||
end
|
||||
|
||||
def enum_value_methods_exist?
|
||||
passed = expected_singleton_methods.all? do |method|
|
||||
model.singleton_methods.include?(method)
|
||||
end
|
||||
|
||||
if passed
|
||||
if instance_methods_exist?
|
||||
true
|
||||
else
|
||||
message = "#{attribute_name.inspect} does map to these "
|
||||
message << 'values, but the enum is '
|
||||
|
||||
if expected_prefix
|
||||
if expected_suffix
|
||||
message << 'configured with either a different prefix or '
|
||||
message << 'suffix, or no prefix or suffix at all'
|
||||
else
|
||||
message << 'configured with either a different prefix or no '
|
||||
message << 'prefix at all'
|
||||
end
|
||||
elsif expected_suffix
|
||||
message << 'configured with either a different suffix or no '
|
||||
message << 'suffix at all'
|
||||
end
|
||||
message = missing_methods_message
|
||||
|
||||
message << " (we can't tell which)"
|
||||
|
||||
|
@ -418,6 +434,68 @@ module Shoulda
|
|||
end
|
||||
end
|
||||
|
||||
def scope_presence_matches?
|
||||
if exclude_scopes?
|
||||
if singleton_methods_exist?
|
||||
message = "#{attribute_name.inspect} does map to these values "
|
||||
message << 'but class scope methods were present'
|
||||
|
||||
@failure_message_continuation = message
|
||||
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
elsif singleton_methods_exist?
|
||||
true
|
||||
else
|
||||
if enum_defined?
|
||||
message = 'But the class scope methods are not present'
|
||||
else
|
||||
message = missing_methods_message
|
||||
|
||||
message << 'or the class scope methods are not present'
|
||||
message << " (we can't tell which)"
|
||||
end
|
||||
|
||||
@failure_message_continuation = message
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def missing_methods_message
|
||||
message = "#{attribute_name.inspect} does map to these "
|
||||
message << 'values, but the enum is '
|
||||
|
||||
if expected_prefix
|
||||
if expected_suffix
|
||||
message << 'configured with either a different prefix or '
|
||||
message << 'suffix, or no prefix or suffix at all'
|
||||
else
|
||||
message << 'configured with either a different prefix or no '
|
||||
message << 'prefix at all'
|
||||
end
|
||||
elsif expected_suffix
|
||||
message << 'configured with either a different suffix or no '
|
||||
message << 'suffix at all'
|
||||
else
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
def singleton_methods_exist?
|
||||
expected_singleton_methods.all? do |method|
|
||||
model.singleton_methods.include?(method)
|
||||
end
|
||||
end
|
||||
|
||||
def instance_methods_exist?
|
||||
expected_instance_methods.all? do |method|
|
||||
record.methods.include?(method)
|
||||
end
|
||||
end
|
||||
|
||||
def expected_singleton_methods
|
||||
expected_enum_value_names.map do |name|
|
||||
[expected_prefix, name, expected_suffix].
|
||||
|
@ -427,6 +505,18 @@ module Shoulda
|
|||
end
|
||||
end
|
||||
|
||||
def expected_instance_methods
|
||||
methods = expected_enum_value_names.map do |name|
|
||||
[expected_prefix, name, expected_suffix].
|
||||
select(&:present?).
|
||||
join('_')
|
||||
end
|
||||
|
||||
methods.flat_map do |m|
|
||||
["#{m}?".to_sym, "#{m}!".to_sym]
|
||||
end
|
||||
end
|
||||
|
||||
def expected_prefix
|
||||
if options.include?(:prefix)
|
||||
if options[:prefix] == true
|
||||
|
@ -447,6 +537,10 @@ module Shoulda
|
|||
end
|
||||
end
|
||||
|
||||
def exclude_scopes?
|
||||
!options[:scopes]
|
||||
end
|
||||
|
||||
def to_hash(value)
|
||||
if value.is_a?(Array)
|
||||
value.each_with_index.inject({}) do |hash, (item, index)|
|
||||
|
|
|
@ -807,6 +807,78 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
if rails_version =~ '~> 6.0'
|
||||
context 'qualified with #without_scopes' do
|
||||
context 'if scopes are set to false on the enum but without_scopes is not used' do
|
||||
it 'has the right description' do
|
||||
record = build_record_with_array_values(
|
||||
attribute_name: :attr,
|
||||
scopes: false,
|
||||
)
|
||||
|
||||
matcher = lambda do
|
||||
expect(record).
|
||||
to define_enum_for(:attr).
|
||||
with_values(['published', 'unpublished', 'draft'])
|
||||
end
|
||||
|
||||
message = format_message(<<-MESSAGE)
|
||||
Expected Example to define :attr as an enum backed by an
|
||||
integer, mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›,
|
||||
and ‹"draft"› to ‹2›. But the class scope methods are not present.
|
||||
MESSAGE
|
||||
|
||||
expect(&matcher).to fail_with_message(message)
|
||||
end
|
||||
end
|
||||
|
||||
context 'if scopes are set to false on the enum' do
|
||||
it 'matches' do
|
||||
record = build_record_with_array_values(
|
||||
attribute_name: :attr,
|
||||
scopes: false,
|
||||
)
|
||||
|
||||
matcher = lambda do
|
||||
define_enum_for(:attr).
|
||||
with_values(['published', 'unpublished', 'draft']).
|
||||
without_scopes
|
||||
end
|
||||
|
||||
expect(&matcher).
|
||||
to match_against(record).
|
||||
or_fail_with(<<-MESSAGE, wrap: true)
|
||||
Expected Example not to define :attr as an enum backed by an
|
||||
integer, mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›,
|
||||
and ‹"draft"› to ‹2› with no scopes, but it did.
|
||||
MESSAGE
|
||||
end
|
||||
end
|
||||
|
||||
context 'if scopes are not set to false on the enum' do
|
||||
it 'has the right description' do
|
||||
record = build_record_with_array_values(attribute_name: :attr)
|
||||
|
||||
matcher = lambda do
|
||||
expect(record).
|
||||
to define_enum_for(:attr).
|
||||
with_values(['published', 'unpublished', 'draft']).
|
||||
without_scopes
|
||||
end
|
||||
|
||||
message = format_message(<<-MESSAGE)
|
||||
Expected Example to define :attr as an enum backed by an
|
||||
integer, mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›,
|
||||
and ‹"draft"› to ‹2› with no scopes. :attr does map to these
|
||||
values but class scope methods were present.
|
||||
MESSAGE
|
||||
|
||||
expect(&matcher).to fail_with_message(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_record_with_array_values(
|
||||
model_name: 'Example',
|
||||
attribute_name: :attr,
|
||||
|
@ -814,7 +886,8 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do
|
|||
values: ['published', 'unpublished', 'draft'],
|
||||
prefix: false,
|
||||
suffix: false,
|
||||
attribute_alias: nil
|
||||
attribute_alias: nil,
|
||||
scopes: true
|
||||
)
|
||||
build_record_with_enum_attribute(
|
||||
model_name: model_name,
|
||||
|
@ -824,6 +897,7 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do
|
|||
prefix: prefix,
|
||||
suffix: suffix,
|
||||
attribute_alias: attribute_alias,
|
||||
scopes: scopes,
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -832,7 +906,8 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do
|
|||
attribute_name: :attr,
|
||||
values: { active: 0, archived: 1 },
|
||||
prefix: false,
|
||||
suffix: false
|
||||
suffix: false,
|
||||
scopes: true
|
||||
)
|
||||
build_record_with_enum_attribute(
|
||||
model_name: model_name,
|
||||
|
@ -841,6 +916,7 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do
|
|||
values: values,
|
||||
prefix: prefix,
|
||||
suffix: suffix,
|
||||
scopes: scopes,
|
||||
attribute_alias: nil,
|
||||
)
|
||||
end
|
||||
|
@ -851,6 +927,7 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do
|
|||
column_type:,
|
||||
values:,
|
||||
attribute_alias:,
|
||||
scopes: true,
|
||||
prefix: false,
|
||||
suffix: false
|
||||
)
|
||||
|
@ -862,7 +939,17 @@ describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do
|
|||
alias_attribute attribute_alias, attribute_name
|
||||
end
|
||||
|
||||
model.enum(enum_name => values, _prefix: prefix, _suffix: suffix)
|
||||
params = {
|
||||
enum_name => values,
|
||||
_prefix: prefix,
|
||||
_suffix: suffix,
|
||||
}
|
||||
|
||||
if rails_version =~ '~> 6.0'
|
||||
params.merge!(_scopes: scopes)
|
||||
end
|
||||
|
||||
model.enum(params)
|
||||
|
||||
model.new
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue