Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add normalize matcher #1558

Merged
merged 1 commit into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/shoulda/matchers/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
require 'shoulda/matchers/active_record/uniqueness'
require 'shoulda/matchers/active_record/validate_uniqueness_of_matcher'
require 'shoulda/matchers/active_record/have_attached_matcher'
require 'shoulda/matchers/active_record/normalize_matcher'

module Shoulda
module Matchers
Expand Down
151 changes: 151 additions & 0 deletions lib/shoulda/matchers/active_record/normalize_matcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
module Shoulda
module Matchers
module ActiveRecord
# The `normalize` matcher is used to ensure attribute normalizations
# are transforming attribute values as expected.
#
# Take this model for example:
#
# class User < ActiveRecord::Base
# normalizes :email, with: -> email { email.strip.downcase }
# end
#
# You can use `normalize` providing an input and defining the expected
# normalization output:
#
# # RSpec
# RSpec.describe User, type: :model do
# it do
# should normalize(:email).from(" [email protected]\n").to("[email protected]")
# end
# end
#
# # Minitest (Shoulda)
# class User < ActiveSupport::TestCase
# should normalize(:email).from(" [email protected]\n").to("[email protected]")
# end
#
# You can use `normalize` to test multiple attributes at once:
#
# class User < ActiveRecord::Base
# normalizes :email, :handle, with: -> value { value.strip.downcase }
# end
#
# # RSpec
# RSpec.describe User, type: :model do
# it do
# should normalize(:email, :handle).from(" Example\n").to("example")
# end
# end
#
# # Minitest (Shoulda)
# class User < ActiveSupport::TestCase
# should normalize(:email, handle).from(" Example\n").to("example")
# end
#
# If the normalization accepts nil values with the `apply_to_nil` option,
# you just need to use `.from(nil).to("Your expected value here")`.
#
# class User < ActiveRecord::Base
# normalizes :name, with: -> name { name&.titleize || 'Untitled' },
# apply_to_nil: true
# end
#
# # RSpec
# RSpec.describe User, type: :model do
# it { should normalize(:name).from("jane doe").to("Jane Doe") }
# it { should normalize(:name).from(nil).to("Untitled") }
# end
#
# # Minitest (Shoulda)
# class User < ActiveSupport::TestCase
# should normalize(:name).from("jane doe").to("Jane Doe")
# should normalize(:name).from(nil).to("Untitled")
# end
#
# @return [NormalizeMatcher]
#
def normalize(*attributes)
if attributes.empty?
raise ArgumentError, 'need at least one attribute'
else
NormalizeMatcher.new(*attributes)
end
end

# @private
class NormalizeMatcher
attr_reader :attributes, :from_value, :to_value, :failure_message,
:failure_message_when_negated

def initialize(*attributes)
@attributes = attributes
end

def description
%(
normalize #{attributes.to_sentence(last_word_connector: ' and ')} from
‹#{from_value.inspect}› to ‹#{to_value.inspect}›
).squish
end

def from(value)
@from_value = value

self
end

def to(value)
@to_value = value

self
end

def matches?(subject)
attributes.all? { |attribute| attribute_matches?(subject, attribute) }
end

def does_not_match?(subject)
attributes.all? { |attribute| attribute_does_not_match?(subject, attribute) }
end

private

def attribute_matches?(subject, attribute)
return true if normalize_attribute?(subject, attribute)

@failure_message = build_failure_message(
attribute,
subject.class.normalize_value_for(attribute, from_value),
)
false
end

def attribute_does_not_match?(subject, attribute)
return true unless normalize_attribute?(subject, attribute)

@failure_message_when_negated = build_failure_message_when_negated(attribute)
false
end

def normalize_attribute?(subject, attribute)
subject.class.normalize_value_for(attribute, from_value) == to_value
end

def build_failure_message(attribute, attribute_value)
%(
Expected to normalize #{attribute.inspect} from ‹#{from_value.inspect}› to
‹#{to_value.inspect}› but it was normalized to ‹#{attribute_value.inspect}›
).squish
end

def build_failure_message_when_negated(attribute)
%(
Expected to not normalize #{attribute.inspect} from ‹#{from_value.inspect}› to
‹#{to_value.inspect}› but it was normalized
).squish
end
end
end
end
end
116 changes: 116 additions & 0 deletions spec/unit/shoulda/matchers/active_record/normalize_matcher_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
require 'unit_spec_helper'

describe Shoulda::Matchers::ActiveRecord::NormalizeMatcher, type: :model do
if rails_version >= 7.1
describe '#description' do
it 'returns the message including the attribute names, from value and to value' do
matcher = normalize(:name, :email).from("jane doe\n").to('Jane Doe')
expect(matcher.description).
to eq('normalize name and email from ‹"jane doe\n"› to ‹"Jane Doe"›')
end
end

context 'when subject normalizes single attribute correctly' do
it 'matches' do
model = define_model(:User, email: :string) do
normalizes :email, with: -> (email) { email.strip.downcase }
end

expect(model.new).to normalize(:email).from(" [email protected]\n").to('[email protected]')
end
end

context 'when subject normalizes multiple attributes correctly' do
it 'matches' do
model = define_model(:User, email: :string, name: :string) do
normalizes :email, :name, with: -> (email) { email.strip.downcase }
end

expect(model.new).to normalize(:email, :name).from(" XyZ\n").to('xyz')
end
end

context 'when subject normalizes single attribute incorrectly' do
it 'fails' do
model = define_model(:User, email: :string) do
normalizes :email, with: -> (email) { email.titleize }
end

assertion = lambda do
expect(model.new).to normalize(:email).from(" [email protected]\n").to('[email protected]')
end

message = %(
Expected to normalize :email from ‹" [email protected]\\n"› to ‹"[email protected]"›
but it was normalized to ‹"Xy [email protected]\\n"›
).squish

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

context 'when subject normalizes just one attribute incorrectly among multiple attributes' do
it 'fails' do
model = define_model(:User, email: :string, name: :string) do
normalizes :name, with: -> (name) { name.titleize.strip }
normalizes :email, with: -> (email) { email.downcase.strip }
end

assertion = lambda do
expect(model.new).to normalize(:name, :email).from(" JaneDoe\n").to('Jane Doe')
end

message = %(
Expected to normalize :email from ‹" JaneDoe\\n"› to ‹"Jane Doe"›
but it was normalized to ‹"janedoe"›
).squish

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

context 'when subject normalize nil values correctly' do
it 'matches' do
model = define_model(:User, name: :string) do
normalizes :name, with: -> (name) { name&.strip || 'Untitled' }, apply_to_nil: true
end

record = model.new

expect(record).to normalize(:name).from(' Jane Doe ').to('Jane Doe')
expect(record).to normalize(:name).from(nil).to('Untitled')
end
end

context "when subject doesn't normalize attribute that it shouldn't normalize" do
it 'does not match' do
model = define_model(:User, email: :string)

expect(model.new).not_to normalize(:email).
from(" [email protected]\n").
to('[email protected]')
end
end

context "when subject normalizes attributes that it shouldn't normalize" do
it 'fails' do
model = define_model(:User, email: :string, name: :string) do
normalizes :email, with: -> (email) { email.strip.downcase }
end

assertion = lambda do
expect(model.new).not_to normalize(:name, :email).
from(" [email protected]\n").
to('[email protected]')
end

message = %(
Expected to not normalize :email from ‹" [email protected]\\n"› to ‹"[email protected]"›
but it was normalized
).squish

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