1
0
Fork 0
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:
John DeWyze 2022-01-31 17:26:50 -06:00 committed by GitHub
parent 14040194ad
commit 38db235515
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 207 additions and 26 deletions

View file

@ -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)|

View file

@ -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