Skip to content

Commit

Permalink
feat: Add default qualifier to define_enum_for matcher
Browse files Browse the repository at this point in the history
On this commit we're adding a new qualifier to the `define_enum_for`
matcher called `with_default`. This qualifier is used to test that the
enum is defined with a default value. A proc can also be passed, and
will be called once each time a new value is needed.

It's nice to note that using Time or Date as the return of Procs as
default value can lead to flaky tests, so it's recommended to
freeze time or date to avoid this.
  • Loading branch information
matsales28 committed Apr 12, 2024
1 parent 33bec1c commit 0af95a0
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 4 deletions.
74 changes: 73 additions & 1 deletion lib/shoulda/matchers/active_record/define_enum_for_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,31 @@ module ActiveRecord
# without_scopes
# end
#
# ##### with_default
#
# Use `with_default` to test that the enum is defined with a
# default value. A proc can also be passed, and will be called once each
# time a new value is needed. (If using Time or Date, it's recommended to
# freeze time or date to avoid flaky tests):
#
# class Issue < ActiveRecord::Base
# enum status: [:open, :closed], default: :closed
# end
#
# # RSpec
# RSpec.describe Issue, type: :model do
# it do
# should define_enum_for(:status).
# with_default(:closed)
# end
# end
#
# # Minitest (Shoulda)
# class ProcessTest < ActiveSupport::TestCase
# should define_enum_for(:status).
# with_default(:closed)
# end
#
# @return [DefineEnumForMatcher]
#
def define_enum_for(attribute_name)
Expand Down Expand Up @@ -247,14 +272,20 @@ def without_scopes
self
end

def with_default(default_value)
options[:default] = default_value
self
end

def matches?(subject)
@record = subject

enum_defined? &&
enum_values_match? &&
column_type_matches? &&
enum_value_methods_exist? &&
scope_presence_matches?
scope_presence_matches? &&
default_value_matches?
end

def failure_message
Expand Down Expand Up @@ -292,6 +323,11 @@ def expectation # rubocop:disable Metrics/MethodLength
)
end

if options[:default].present?
expectation << ', with a default value of '
expectation << Shoulda::Matchers::Util.inspect_value(expected_default_value)
end

if expected_prefix
expectation <<
if expected_suffix
Expand Down Expand Up @@ -476,6 +512,42 @@ def missing_methods_message
end
end

def default_value_matches?
return true if options[:default].blank?

if actual_default_value.nil?
@failure_message_continuation = 'However, no default value was set'
return false
end

if actual_default_value == expected_default_value
true
else
@failure_message_continuation = 'However, the default value is '
@failure_message_continuation << Shoulda::Matchers::Util.inspect_value(
actual_default_value,
)
false
end
end

def expected_default_value
options[:default].respond_to?(:call) ? options[:default].call : options[:default]
end

def actual_default_value
attribute_schema = model.attributes_to_define_after_schema_loads[attribute_name.to_s]

value = case attribute_schema
in [_, { default: default_value } ]
default_value
in [_, default_value]
default_value
end

value.respond_to?(:call) ? value.call : value
end

def singleton_methods_exist?
expected_singleton_methods.all? do |method|
model.singleton_methods.include?(method)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,122 @@ def self.statuses
end
end

describe 'qualified with #with_default' do
context 'if default are defined on the enum' do
context 'but with_default is not used' do
it 'matches' do
record = build_record_with_array_values(attribute_name: :attr, default: 'published')

expect(record).to define_enum_for(:attr).with_values(['published', 'unpublished', 'draft'])
end
end

context 'with_default is used and default is the same' do
it 'matches' do
record = build_record_with_array_values(attribute_name: :attr, default: 'published')

matcher = lambda do
define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
with_default('published')
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 a default value of ‹"published"›, but it did.
MESSAGE
end
end

context 'with_default is used but default is different' do
it 'rejects with an appropriate failure message' do
record = build_record_with_array_values(attribute_name: :attr, default: 'published')

assertion = lambda do
expect(record).
to define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
with_default('unpublished')
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 a default value of ‹"unpublished"›. However, the default value
is ‹"published"›.
MESSAGE

expect(&assertion).to fail_with_message(message)
end
end

context 'when a Proc is used as the default value' do
it 'rejects with an appropriate failure message' do
record = build_record_with_array_values(attribute_name: :attr, default: 'draft')

assertion = lambda do
expect(record).
to define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
with_default(-> { 'published' })
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 a default value of ‹"published"›. However, the default
value is ‹"draft"›.
MESSAGE

expect(&assertion).to fail_with_message(message)
end

it 'matches when the default value is the same' do
record = build_record_with_array_values(attribute_name: :attr, default: 'draft')

matcher = lambda do
define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
with_default(-> { 'draft' })
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 a default value of ‹"draft"›, but it did.
MESSAGE
end
end
end

context 'if default is not defined on the enum' do
it 'rejects with an appropriate failure message' do
record = build_record_with_array_values(attribute_name: :attr)

assertion = lambda do
expect(record).
to define_enum_for(:attr).
with_values(['published', 'unpublished', 'draft']).
with_default('published')
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 a default value of ‹"published"›. However, no default
value was set.
MESSAGE

expect(&assertion).to fail_with_message(message)
end
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
Expand Down Expand Up @@ -869,7 +985,8 @@ def build_record_with_array_values(
prefix: false,
suffix: false,
attribute_alias: nil,
scopes: true
scopes: true,
default: nil
)
build_record_with_enum_attribute(
model_name: model_name,
Expand All @@ -880,6 +997,7 @@ def build_record_with_array_values(
suffix: suffix,
attribute_alias: attribute_alias,
scopes: scopes,
default: default,
)
end

Expand Down Expand Up @@ -911,7 +1029,8 @@ def build_record_with_enum_attribute(
attribute_alias:,
scopes: true,
prefix: false,
suffix: false
suffix: false,
default: nil
)
enum_name = attribute_alias || attribute_name
model = define_model(
Expand All @@ -925,10 +1044,11 @@ def build_record_with_enum_attribute(
enum_name => values,
_prefix: prefix,
_suffix: suffix,
_default: default,
}

if rails_version >= 7.0
model.enum(enum_name, values, prefix: prefix, suffix: suffix)
model.enum(enum_name, values, prefix: prefix, suffix: suffix, default: default)
else
params.merge!(_scopes: scopes)
model.enum(params)
Expand Down

0 comments on commit 0af95a0

Please sign in to comment.