Skip to content

Commit

Permalink
Improve failure messages for define_enum_for
Browse files Browse the repository at this point in the history
If a subtest fails, explain why. We're going to be adding another
subtest later, and so without this extra information, users will most
definitely be confused.
  • Loading branch information
mcmire committed Jan 28, 2018
1 parent a05ad14 commit d8681fa
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 77 deletions.
131 changes: 95 additions & 36 deletions lib/shoulda/matchers/active_record/define_enum_for_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -118,57 +132,102 @@ 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
model.send(attribute_name.to_s.pluralize)
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
Expand All @@ -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
Expand Down
23 changes: 22 additions & 1 deletion lib/shoulda/matchers/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)]
Expand Down
8 changes: 6 additions & 2 deletions lib/shoulda/matchers/util/word_wrap.rb
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
13 changes: 13 additions & 0 deletions spec/support/unit/helpers/message_helpers.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit d8681fa

Please sign in to comment.