From 648e297cbf9a2f34bac96bf2604c4f34546c7e44 Mon Sep 17 00:00:00 2001 From: Daniela Velasquez Date: Thu, 9 May 2024 21:49:13 +0000 Subject: [PATCH] Adds `assert_error_on` and `assert_no_error_on` assertions Introduces two new assertions, `assert_error_on` and `assert_no_error_on`, to simplify checking for specific validation errors on models. Example usage: - assert_error_on user, :name, :blank - assert_no_error_on user, :name, :blank This enhances test readability and makes validation testing more intuitive. --- activesupport/CHANGELOG.md | 11 ++- .../lib/active_support/testing/assertions.rb | 80 +++++++++++++++++++ activesupport/test/testing/assertions_test.rb | 78 ++++++++++++++++++ 3 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 activesupport/test/testing/assertions_test.rb diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 7815176af540..c25e6fd2984d 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,12 @@ +* Added new test assertions `assert_error_on` and `assert_no_error_on` to simplify testing for specific validation errors on models. + Example usage: + ```ruby + assert_error_on user :name, :blank + assert_no_error user, :name, :blank + ``` + + *Daniela Velasquez* + * Fix `ActiveSupport::HashWithIndifferentAccess#stringify_keys` to stringify all keys not just symbols. Previously: @@ -104,4 +113,4 @@ *mopp* -Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/activesupport/CHANGELOG.md) for previous changes. +Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/activesupport/CHANGELOG.md) for previous changes. \ No newline at end of file diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 741477ab2265..b5d6de9b6f44 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -282,6 +282,86 @@ def assert_no_changes(expression, message = nil, from: UNTRACKED, &block) retval end + # Asserts that an ActiveModel object has a specific error on a given attribute. + # + # This assertion checks whether a validation error of a specific type + # exists on the provided attribute of the object. If the error does not + # exist, the test will fail, displaying an optional custom error message or + # a default one indicating the expected and actual outcomes. + # + # Example: + # + # assert_error_on(user, :name, :blank) + # # Asserts that the `user` object has a `:blank` validation error on the `:name` attribute. + # + # Arguments: + # obj: The ActiveModel object being validated. It must respond to `#errors`. + # attribute: The attribute for which the validation error is expected (e.g., `:name`). + # type: The specific type of validation error (e.g., `:blank`, `:invalid`, etc.). + # msg: An optional custom message to display if the assertion fails. If not provided, + # a default message is generated based on the provided attribute and error type. + # + # Raises: + # ArgumentError: If the `obj` does not respond to `#errors`. + # + # Example Custom Error Message: + # + # assert_error_on(user, :name, :blank, "Expected 'name' to be present but it's blank.") + # + # Failing Example: + # + # assert_error_on(user, :email, :invalid) + # # Fails if `user.errors` does not contain an `:invalid` error for the `:email` attribute. + def assert_error_on(obj, attribute, type, msg = nil) + raise ArgumentError.new("#{obj.inspect} does not respond to #errors") unless obj.respond_to?(:errors) + + msg = message(msg) { + data = [type, attribute] + "Expected error %s on %s" % data + } + + assert(obj.errors.added?(attribute, type), msg) + end + + # Asserts that an ActiveModel object does not have a specific error on a given attribute. + # + # This assertion checks that a validation error of a specific type is not present + # on the provided attribute of the object. If the error exists, the test will fail, + # displaying an optional custom error message or a default one indicating the + # unexpected error on the attribute. + # + # Example: + # + # assert_no_error_on(user, :name, :blank) + # # Asserts that the `user` object does not have a `:blank` validation error on the `:name` attribute. + # + # Arguments: + # obj: The ActiveModel object being validated. It must respond to `#errors`. + # attribute: The attribute for which the absence of a specific validation error is expected (e.g., `:name`). + # type: The specific type of validation error that is expected *not* to be present (e.g., `:blank`, `:invalid`). + # msg: An optional custom message to display if the assertion fails. If not provided, + # a default message is generated based on the provided attribute and error type. + # + # Raises: + # ArgumentError: If the `obj` does not respond to `#errors`. + # + # Example Custom Error Message: + # + # assert_no_error_on(user, :name, :blank, "Expected 'name' to be valid and not blank.") + # + # Failing Example: + # + # assert_no_error_on(user, :email, :invalid) + # # Fails if `user.errors` contains an `:invalid` error for the `:email` attribute. + def assert_no_error_on(obj, attribute, type, msg=nil) + raise ArgumentError.new("#{obj.inspect} does not respond to #errors") unless obj.respond_to?(:errors) + msg = message(msg) { + data = [attribute, type] + "Expected %s to not be %s" % data + } + assert_not(obj.errors.added?(attribute, type), msg) + end + private def _assert_nothing_raised_or_warn(assertion, &block) assert_nothing_raised(&block) diff --git a/activesupport/test/testing/assertions_test.rb b/activesupport/test/testing/assertions_test.rb new file mode 100644 index 000000000000..45cf91f68dfa --- /dev/null +++ b/activesupport/test/testing/assertions_test.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require_relative "../abstract_unit" +require "active_model" + +class AssertionsTest < ActiveSupport::TestCase + def setup + @active_model = Class.new do + include ActiveModel::Model + include ActiveModel::Attributes + + attribute :name, :string + attribute :last_name, :string + validates :name, :last_name, presence: true + validate :name_doesnt_contain_numbers + + private + def name_doesnt_contain_numbers + unless name.nil? || name.scan(/\d/).empty? + errors.add(:name, "shouldn't contain numbers") + end + end + end.new + end + + test "#assert_no_error_on asserts active model does not have an error on a field" do + @active_model.name = "name" + @active_model.validate + + assert_no_error_on @active_model, :name, :blank + end + + test "#assert_no_error_on raises ArgumentError with an object that doesn't respond to errors" do + error = assert_raises(ArgumentError) do + assert_no_error_on Object.new, :name, :blank, Object.new + end + + assert_includes error.message, "does not respond to #errors" + end + + test "#assert_no_error_on raises a Minitest::Assertion when validation fails" do + @active_model.validate + error = assert_raises(Minitest::Assertion) do + assert_no_error_on @active_model, :name, :blank + end + assert_includes error.message, "Expected name to not be blank" + end + + test "#assert_error_on asserts active model has an error on name field" do + @active_model.validate + assert_error_on @active_model, :name, :blank + end + + test "#assert_error_on asserts active model has an error on a field with a string" do + error_message = "must start with H" + @active_model.errors.add(:name, error_message) + + assert_error_on @active_model, :name, error_message + end + + test "#assert_error_on raises ArgumentError on an object that doesn't respond to errors" do + error = assert_raises(ArgumentError) do + assert_error_on :name, :blank, Object.new + end + + assert_includes error.message, "does not respond to #errors" + end + + test "#assert_error_on raises a Minitest::Assertion when validation fails" do + @active_model.name = "h" + @active_model.validate + error = assert_raises(Minitest::Assertion) do + assert_error_on :name, :blank, @active_model + end + assert_includes error.message, "Expected error blank on name" + credit123$ + end +end