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 95a7cb59c..7ac46a9cd 100644 --- a/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +++ b/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb @@ -91,7 +91,21 @@ def define_enum_for(attribute_name) class DefineEnumForMatcher def initialize(attribute_name) @attribute_name = attribute_name - @options = {} + @options = { expected_enum_values: [] } + end + + def description + description = "define :#{attribute_name} as an enum, backed by " + description << Shoulda::Matchers::Util.a_or_an(expected_column_type) + + if presented_expected_enum_values.any? + description << ', with possible values ' + description << Shoulda::Matchers::Util.inspect_value( + presented_expected_enum_values, + ) + end + + description end def with_values(expected_enum_values) @@ -102,7 +116,7 @@ def with_values(expected_enum_values) def with(expected_enum_values) Shoulda::Matchers.warn_about_deprecated_method( 'The `with` qualifier on `define_enum_for`', - '`with_values`' + '`with_values`', ) with_values(expected_enum_values) end @@ -118,37 +132,56 @@ def matches?(subject) end def failure_message - "Expected #{expectation}" + message = "Expected #{model} to #{expectation}" + + if failure_reason + message << ". However, #{failure_reason}" + end + + message << '.' + + Shoulda::Matchers.word_wrap(message) end - alias :failure_message_for_should :failure_message def failure_message_when_negated - "Did not expect #{expectation}" + message = "Expected #{model} not to #{expectation}, but it did." + Shoulda::Matchers.word_wrap(message) end - alias :failure_message_for_should_not :failure_message_when_negated - def description - desc = "define :#{attribute_name} as an enum" + private - if options[:expected_enum_values] - desc << " with #{options[:expected_enum_values]}" - end + attr_reader :attribute_name, :options, :record, :failure_reason - desc << " and store the value in a column of type #{expected_column_type}" + def expectation + description + end - desc + def presented_expected_enum_values + if expected_enum_values.is_a?(Hash) + expected_enum_values.symbolize_keys + else + expected_enum_values + end end - protected + def normalized_expected_enum_values + to_hash(expected_enum_values) + end - attr_reader :record, :attribute_name, :options + def expected_enum_values + options[:expected_enum_values] + end - def expectation - "#{model.name} to #{description}" + def presented_actual_enum_values + if expected_enum_values.is_a?(Array) + to_array(actual_enum_values) + else + to_hash(actual_enum_values).symbolize_keys + end end - def expected_enum_values - hashify(options[:expected_enum_values]).with_indifferent_access + def normalized_actual_enum_values + to_hash(actual_enum_values) end def actual_enum_values @@ -156,19 +189,45 @@ def actual_enum_values end def enum_defined? - model.defined_enums.include?(attribute_name.to_s) + if model.defined_enums.include?(attribute_name.to_s) + true + else + @failure_reason = "no such enum exists in #{model}" + false + end end def enum_values_match? - expected_enum_values.empty? || actual_enum_values == expected_enum_values - end + passed = + expected_enum_values.empty? || + normalized_actual_enum_values == normalized_expected_enum_values - def expected_column_type - options[:expected_column_type] || :integer + if passed + true + else + @failure_reason = + "the actual enum values for #{attribute_name.inspect} are " + + Shoulda::Matchers::Util.inspect_value( + presented_actual_enum_values, + ) + false + end end def column_type_matches? - column.type == expected_column_type.to_sym + if column.type == expected_column_type.to_sym + true + else + @failure_reason = + "#{attribute_name.inspect} is " + + Shoulda::Matchers::Util.a_or_an(column.type) + + ' column' + false + end + end + + def expected_column_type + options[:expected_column_type] || :integer end def column @@ -179,21 +238,21 @@ def model record.class end - def hashify(value) - if value.nil? - return {} - end - + def to_hash(value) if value.is_a?(Array) - new_value = {} - - value.each_with_index do |v, i| - new_value[v] = i + value.each_with_index.inject({}) do |hash, (item, index)| + hash.merge(item.to_s => index) end + else + value.stringify_keys + end + end - new_value + def to_array(value) + if value.is_a?(Array) + value.map(&:to_s) else - value + value.keys.map(&:to_s) end end end diff --git a/lib/shoulda/matchers/util.rb b/lib/shoulda/matchers/util.rb index 36ac126e1..df828bf1f 100644 --- a/lib/shoulda/matchers/util.rb +++ b/lib/shoulda/matchers/util.rb @@ -41,7 +41,14 @@ def self.a_or_an(next_word) end def self.inspect_value(value) - "‹#{value.inspect}›" + case value + when Hash + inspect_hash(value) + when Range + inspect_range(value) + else + "‹#{value.inspect}›" + end end def self.inspect_values(values) @@ -52,6 +59,20 @@ def self.inspect_range(range) "#{inspect_value(range.first)} to #{inspect_value(range.last)}" end + def self.inspect_hash(hash) + output = '‹{' + + output << hash.map { |key, value| + if key.is_a?(Symbol) + "#{key}: #{value.inspect}" + else + "#{key.inspect} => #{value.inspect}" + end + }.join(', ') + + output << '}›' + end + def self.dummy_value_for(column_type, array: false) if array [dummy_value_for(column_type, array: false)] diff --git a/lib/shoulda/matchers/util/word_wrap.rb b/lib/shoulda/matchers/util/word_wrap.rb index 2f3bfda30..7e73f6acb 100644 --- a/lib/shoulda/matchers/util/word_wrap.rb +++ b/lib/shoulda/matchers/util/word_wrap.rb @@ -1,10 +1,14 @@ module Shoulda module Matchers # @private - def self.word_wrap(document, options = {}) - Document.new(document, options).wrap + module WordWrap + def word_wrap(document, options = {}) + Document.new(document, options).wrap + end end + extend WordWrap + # @private class Document def initialize(document, indent: 0) diff --git a/spec/support/unit/helpers/message_helpers.rb b/spec/support/unit/helpers/message_helpers.rb new file mode 100644 index 000000000..c0e7d3ff5 --- /dev/null +++ b/spec/support/unit/helpers/message_helpers.rb @@ -0,0 +1,13 @@ +module UnitTests + module MessageHelpers + include Shoulda::Matchers::WordWrap + + def self.configure_example_group(example_group) + example_group.include(self) + end + + def format_message(message) + word_wrap(message.strip_heredoc.strip) + end + 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 7186ee1ca..d05699ae1 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 @@ -8,9 +8,9 @@ attribute_name: :attr, column_type: :integer, ) - message = as_one_line(<<~MESSAGE) - Expected Example to define :attrs as an enum and store the value in a - column of type integer + message = format_message(<<-MESSAGE) + Expected Example to define :attrs as an enum, backed by an integer. + However, no such enum exists in Example. MESSAGE assertion = lambda do @@ -27,13 +27,13 @@ def self.statuses; end end - message = as_one_line(<<~MESSAGE) - Expected Example to define :statuses as an enum and store the value in a - column of type integer + message = format_message(<<-MESSAGE) + Expected Example to define :attr as an enum, backed by an integer. + However, no such enum exists in Example. MESSAGE assertion = lambda do - expect(model.new).to define_enum_for(:statuses) + expect(model.new).to define_enum_for(:attr) end expect(&assertion).to fail_with_message(message) @@ -47,9 +47,9 @@ def self.statuses; end model_name: 'Example', attribute_name: :attr, ) - message = as_one_line(<<~MESSAGE) - Expected Example to define :attr as an enum and store the value in a - column of type integer + message = format_message(<<-MESSAGE) + Expected Example to define :attr as an enum, backed by an integer. + However, no such enum exists in Example. MESSAGE assertion = lambda do @@ -67,9 +67,9 @@ def self.statuses; end attribute_name: :attr, column_type: :string, ) - message = as_one_line(<<~MESSAGE) - Expected Example to define :attr as an enum and store the value in a - column of type integer + message = format_message(<<-MESSAGE) + Expected Example to define :attr as an enum, backed by an integer. + However, :attr is a string column. MESSAGE assertion = lambda do @@ -94,9 +94,9 @@ def self.statuses; end attribute_name: :attr, column_type: :integer, ) - message = as_one_line(<<~MESSAGE) - Did not expect Example to define :attr as an enum and store the - value in a column of type integer + message = format_message(<<-MESSAGE) + Expected Example not to define :attr as an enum, backed by an integer, + but it did. MESSAGE assertion = lambda do @@ -117,20 +117,23 @@ def self.statuses; end model_name: 'Example', attribute_name: :attr, ) - message = as_one_line(<<~MESSAGE) - Expected Example to define :attr as an enum with ["open", "close"] - and store the value in a column of type integer + message = format_message(<<-MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + with possible values ‹["open", "close"]›. However, no such enum + exists in Example. MESSAGE assertion = lambda do - expect(record).to define_enum_for(:attr).with_values(['open', 'close']) + expect(record). + to define_enum_for(:attr). + with_values(['open', 'close']) end expect(&assertion).to fail_with_message(message) end end - context 'if the attribute is defined as an enum and the enum values match' do + context 'if the attribute is defined as an enum' do context 'but the enum values do not match' do it 'rejects with an appropriate failure message' do record = build_record_with_array_values( @@ -138,13 +141,16 @@ def self.statuses; end attribute_name: :attr, values: ['published', 'unpublished', 'draft'], ) - message = as_one_line(<<~MESSAGE) - Expected Example to define :attr as an enum with ["open", "close"] - and store the value in a column of type integer + message = format_message(<<-MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + with possible values ‹["open", "close"]›. However, the actual + enum values for :attr are ‹["published", "unpublished", "draft"]›. MESSAGE assertion = lambda do - expect(record).to define_enum_for(:attr).with_values(['open', 'close']) + expect(record). + to define_enum_for(:attr). + with_values(['open', 'close']) end expect(&assertion).to fail_with_message(message) @@ -172,9 +178,10 @@ def self.statuses; end model_name: 'Example', attribute_name: :attr, ) - message = as_one_line(<<~MESSAGE) - Expected Example to define :attr as an enum with {:active=>5, - :archived=>10} and store the value in a column of type integer + message = format_message(<<-MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + with possible values ‹{active: 5, archived: 10}›. However, no such + enum exists in Example. MESSAGE assertion = lambda do @@ -195,9 +202,10 @@ def self.statuses; end attribute_name: :attr, values: { active: 0, archived: 1 }, ) - message = as_one_line(<<~MESSAGE) - Expected Example to define :attr as an enum with {:active=>5, - :archived=>10} and store the value in a column of type integer + message = format_message(<<-MESSAGE) + Expected Example to define :attr as an enum, backed by an integer, + with possible values ‹{active: 5, archived: 10}›. However, the + actual enum values for :attr are ‹{active: 0, archived: 1}›. MESSAGE assertion = lambda do @@ -267,9 +275,9 @@ def self.statuses; end attribute_name: :attr, column_type: :integer, ) - message = as_one_line(<<~MESSAGE) - Expected Example to define :attr as an enum and store the value in a - column of type string + message = format_message(<<-MESSAGE) + Expected Example to define :attr as an enum, backed by a string. + However, :attr is an integer column. MESSAGE assertion = lambda do @@ -340,8 +348,4 @@ def build_record_with_enum_attribute( def build_record_with_non_enum_attribute(model_name:, attribute_name:) define_model(model_name, attribute_name => :integer).new end - - def as_one_line(message) - message.tr("\n", ' ').strip - end end diff --git a/spec/unit_spec_helper.rb b/spec/unit_spec_helper.rb index dfbe21d63..26c832ad3 100644 --- a/spec/unit_spec_helper.rb +++ b/spec/unit_spec_helper.rb @@ -24,6 +24,7 @@ UnitTests::DatabaseHelpers.configure_example_group(config) UnitTests::ColumnTypeHelpers.configure_example_group(config) UnitTests::ValidationMatcherScenarioHelpers.configure_example_group(config) + UnitTests::MessageHelpers.configure_example_group(config) if UnitTests::RailsVersions.rails_lte_4? UnitTests::ActiveResourceBuilder.configure_example_group(config)