From 46632cea4aed75bcb5ed5175204b3ca7fecf6f01 Mon Sep 17 00:00:00 2001 From: vaot Date: Wed, 12 Jun 2024 11:22:12 -0700 Subject: [PATCH] feat: Add without_instance_methods qualifier to enum matcher --- .../active_record/define_enum_for_matcher.rb | 46 +++++++++-- .../define_enum_for_matcher_spec.rb | 78 ++++++++++++++++++- 2 files changed, 115 insertions(+), 9 deletions(-) diff --git a/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb b/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb index 8b583a4e3..65aacd2d4 100644 --- a/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +++ b/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb @@ -253,8 +253,29 @@ module ActiveRecord # validating(allowing_nil: true) # end # - # @return [DefineEnumForMatcher] + # ##### without_instance_methods + # + # Use `without_instance_methods` to exclude the check for instance methods. + # + # class Issue < ActiveRecord::Base + # enum status: [:open, :closed], instance_methods: false + # end + # + # # RSpec + # RSpec.describe Issue, type: :model do + # it do + # should define_enum_for(:status). + # without_instance_methods + # end + # end + # + # # Minitest (Shoulda) + # class ProcessTest < ActiveSupport::TestCase + # should define_enum_for(:status). + # without_instance_methods + # end # + # @return [DefineEnumForMatcher] def define_enum_for(attribute_name) DefineEnumForMatcher.new(attribute_name) end @@ -263,7 +284,7 @@ def define_enum_for(attribute_name) class DefineEnumForMatcher def initialize(attribute_name) @attribute_name = attribute_name - @options = { expected_enum_values: [], scopes: true } + @options = { expected_enum_values: [], scopes: true, instance_methods: true } end def description @@ -319,6 +340,11 @@ def without_scopes self end + def without_instance_methods + options[:instance_methods] = false + self + end + def with_default(default_value) options[:default] = default_value self @@ -531,16 +557,24 @@ def model end def enum_value_methods_exist? - if instance_methods_exist? - true - else - message = missing_methods_message + if options[:instance_methods] + return true if instance_methods_exist? + message = missing_methods_message message << " (we can't tell which)" + @failure_message_continuation = message + + false + elsif instance_methods_exist? + message = missing_methods_message + + message << '. Expected no instance methods, but instance methods were present' @failure_message_continuation = message false + else + true end end diff --git a/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb index 06093c244..bc3e273aa 100644 --- a/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb @@ -1114,6 +1114,75 @@ def self.statuses end end end + + describe 'qualified with #without_instance_methods' do + context 'if instance methods are set to false on the enum but without_instance_methods is not used' do + it 'rejects with failure message' do + record = build_record_with_array_values( + attribute_name: :attr, + instance_methods: 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›. (we can't tell which). + MESSAGE + + expect(&matcher).to fail_with_message(message) + end + end + + context 'if instance methods are set to false on the enum' do + it 'matches' do + record = build_record_with_array_values( + attribute_name: :attr, + instance_methods: false, + ) + + matcher = lambda do + define_enum_for(:attr). + with_values(['published', 'unpublished', 'draft']). + without_instance_methods + end + + expect(&matcher). + to match_against(record). + or_fail_with(<<-MESSAGE) + Expected Example not to define :attr as an enum backed by an integer, + mapping ‹"published"› to ‹0›, ‹"unpublished"› to ‹1›, and ‹"draft"› to + ‹2›, but it did. + MESSAGE + end + end + + context 'if instance methods are not set to false on the enum' do + it 'rejects with failure message' 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_instance_methods + 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›. . Expected no instance methods, but instance methods were present. + MESSAGE + + expect(&matcher).to fail_with_message(message) + end + end + end end if rails_version =~ '~> 6.0' @@ -1198,7 +1267,8 @@ def build_record_with_array_values( attribute_alias: nil, scopes: true, default: nil, - validate: false + validate: false, + instance_methods: true ) build_record_with_enum_attribute( model_name: model_name, @@ -1211,6 +1281,7 @@ def build_record_with_array_values( scopes: scopes, default: default, validate: validate, + instance_methods: instance_methods ) end @@ -1244,7 +1315,8 @@ def build_record_with_enum_attribute( prefix: false, suffix: false, default: nil, - validate: false + validate: false, + instance_methods: true ) enum_name = attribute_alias || attribute_name model = define_model( @@ -1262,7 +1334,7 @@ def build_record_with_enum_attribute( } if rails_version >= 7.0 - model.enum(enum_name, values, prefix: prefix, suffix: suffix, validate: validate, default: default) + model.enum(enum_name, values, prefix: prefix, suffix: suffix, validate: validate, default: default, instance_methods: instance_methods) else params.merge!(_scopes: scopes) model.enum(params)