Created
February 13, 2018 04:07
-
-
Save GuyPaddock/707126abc6589c14db756f1a4f4835ae to your computer and use it in GitHub Desktop.
An RSpec matcher for doing precise checks on active model validations errors.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## | |
# An RSpec matcher for checking Rails validation errors on an object. | |
# | |
# Usage: | |
# # Expect exactly three validation errors: | |
# # - field1 must not be empty | |
# # - field1 must be a number | |
# # - field2 must be greater than zero | |
# expect(x).to have_validation_errors | |
# .related_to(:field1) | |
# .that_say("must not be empty", "must be a number") | |
# .and_others | |
# .related_to(:field2) | |
# .that_say("must be greater than zero") | |
# | |
# # Expect no validation errors | |
# expect(x).not_to have_validation_errors | |
# | |
# # Expect no validation errors on field1 that say | |
# # "field1 must not be empty" or "field1 must be a number" | |
# expect(x).not_to have_validation_errors | |
# .related_to(:field1) | |
# .that_say("must not be empty", "must be a number") | |
# | |
RSpec::Matchers.define :have_validation_errors do | |
match do |actual| | |
total_error_count = self.total_error_count | |
if actual.respond_to?(:errors) | |
if total_error_count > 0 | |
(actual.errors.size == total_error_count) && all_errors_match?(actual) | |
else | |
actual.errors.size > 0 | |
end | |
end | |
end | |
match_when_negated do |actual| | |
total_error_count = self.total_error_count | |
!actual.respond_to?(:errors) || | |
((total_error_count > 0) && all_errors_excluded?(actual)) || | |
(actual.errors.size == 0) | |
end | |
failure_message do |actual| | |
super_message = super() | |
if actual.respond_to?(:errors) && actual.errors | |
"#{super_message}, but got #{actual.errors.full_messages}" | |
else | |
"#{super_message}, but got no validation errors." | |
end | |
end | |
failure_message_when_negated do |actual| | |
super_message = super() | |
"#{super_message}, but got #{actual.errors.full_messages}" | |
end | |
chain :related_to do |field| | |
@field_errors ||= {} | |
@field_errors[field] ||= [] | |
@last_field = field | |
end | |
chain :that_say do |*messages| | |
unless @last_field | |
raise 'last_field must be preceded by related_to.' | |
end | |
@field_errors[@last_field] += messages | |
end | |
chain :and_others do | |
# This is a placeholder for fluency | |
end | |
## | |
# Calculates the total number of expected validation errors across all of the | |
# fields. | |
# | |
# @return [Integer] | |
# | |
def total_error_count | |
if @field_errors | |
@field_errors.inject(0) { |memo, (field, errors)| memo + errors.size } | |
else | |
0 | |
end | |
end | |
## | |
# Matches the errors on the provided actual object against this matcher's | |
# expectations. | |
# | |
# @param [ActiveModel::Validations] actual | |
# The object whose validation errors will be matched against this object. | |
# | |
def all_errors_match?(actual) | |
@field_errors.all? do |(field_name, expected_errors)| | |
actual_errors = actual.errors[field_name] | |
if actual_errors | |
expected_errors.all? do |expected_error| | |
actual_errors.include? expected_error | |
end | |
end | |
end | |
end | |
## | |
# Ensures the errors on the provided actual object exclude all of the ones | |
# in this matcher's expectations. | |
# | |
# @param [ActiveModel::Validations] actual | |
# The object whose validation errors will be matched against this object. | |
# | |
def all_errors_excluded?(actual) | |
@field_errors.all? do |(field_name, excluded_errors)| | |
actual_errors = actual.errors[field_name] | |
actual_errors.blank? || excluded_errors.none? do |excluded_error| | |
actual_errors.include? excluded_error | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment