Add have_implicit_order_column matcher
Add a matcher that can test the new [implicit_order_column][1] class property that is available on ActiveRecord classes in Rails 6. [1]: https://github.com/rails/rails/pull/34480 Co-authored-by: Elliot Winkler <elliot.winkler@gmail.com>
This commit is contained in:
parent
c956f6a44d
commit
f82329a679
|
@ -14,6 +14,7 @@ require "shoulda/matchers/active_record/association_matchers/model_reflection"
|
||||||
require "shoulda/matchers/active_record/association_matchers/option_verifier"
|
require "shoulda/matchers/active_record/association_matchers/option_verifier"
|
||||||
require "shoulda/matchers/active_record/have_db_column_matcher"
|
require "shoulda/matchers/active_record/have_db_column_matcher"
|
||||||
require "shoulda/matchers/active_record/have_db_index_matcher"
|
require "shoulda/matchers/active_record/have_db_index_matcher"
|
||||||
|
require "shoulda/matchers/active_record/have_implicit_order_column"
|
||||||
require "shoulda/matchers/active_record/have_readonly_attribute_matcher"
|
require "shoulda/matchers/active_record/have_readonly_attribute_matcher"
|
||||||
require "shoulda/matchers/active_record/have_rich_text_matcher"
|
require "shoulda/matchers/active_record/have_rich_text_matcher"
|
||||||
require "shoulda/matchers/active_record/have_secure_token_matcher"
|
require "shoulda/matchers/active_record/have_secure_token_matcher"
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
module Shoulda
|
||||||
|
module Matchers
|
||||||
|
module ActiveRecord
|
||||||
|
# The `have_implicit_order_column` matcher tests that the model has `implicit_order_column`
|
||||||
|
# assigned to one of the table columns. (Rails 6+ only)
|
||||||
|
#
|
||||||
|
# class Product < ApplicationRecord
|
||||||
|
# self.implicit_order_column = :created_at
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# # RSpec
|
||||||
|
# RSpec.describe Product, type: :model do
|
||||||
|
# it { should have_implicit_order_column(:created_at) }
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# # Minitest (Shoulda)
|
||||||
|
# class ProductTest < ActiveSupport::TestCase
|
||||||
|
# should have_implicit_order_column(:created_at)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# @return [HaveImplicitOrderColumnMatcher]
|
||||||
|
#
|
||||||
|
if RailsShim.active_record_gte_6?
|
||||||
|
def have_implicit_order_column(column_name)
|
||||||
|
HaveImplicitOrderColumnMatcher.new(column_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# @private
|
||||||
|
class HaveImplicitOrderColumnMatcher
|
||||||
|
attr_reader :failure_message
|
||||||
|
|
||||||
|
def initialize(column_name)
|
||||||
|
@column_name = column_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def matches?(subject)
|
||||||
|
@subject = subject
|
||||||
|
check_column_exists!
|
||||||
|
check_implicit_order_column_matches!
|
||||||
|
true
|
||||||
|
rescue SecondaryCheckFailedError => error
|
||||||
|
@failure_message = Shoulda::Matchers.word_wrap(
|
||||||
|
"Expected #{model.name} to #{expectation}, " +
|
||||||
|
"but that could not be proved: #{error.message}."
|
||||||
|
)
|
||||||
|
false
|
||||||
|
rescue PrimaryCheckFailedError => error
|
||||||
|
@failure_message = Shoulda::Matchers.word_wrap(
|
||||||
|
"Expected #{model.name} to #{expectation}, but #{error.message}."
|
||||||
|
)
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def failure_message_when_negated
|
||||||
|
Shoulda::Matchers.word_wrap(
|
||||||
|
"Expected #{model.name} not to #{expectation}, but it did."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def description
|
||||||
|
expectation
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :column_name, :subject
|
||||||
|
|
||||||
|
def check_column_exists!
|
||||||
|
matcher = HaveDbColumnMatcher.new(column_name)
|
||||||
|
|
||||||
|
if !matcher.matches?(@subject)
|
||||||
|
raise SecondaryCheckFailedError.new(
|
||||||
|
"The :#{model.table_name} table does not have a " +
|
||||||
|
":#{column_name} column"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_implicit_order_column_matches!
|
||||||
|
if model.implicit_order_column.to_s != column_name.to_s
|
||||||
|
message =
|
||||||
|
if model.implicit_order_column.nil?
|
||||||
|
"implicit_order_column is not set"
|
||||||
|
else
|
||||||
|
"it is :#{model.implicit_order_column}"
|
||||||
|
end
|
||||||
|
|
||||||
|
raise PrimaryCheckFailedError.new(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def model
|
||||||
|
subject.class
|
||||||
|
end
|
||||||
|
|
||||||
|
def expectation
|
||||||
|
"have an implicit_order_column of :#{column_name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
class SecondaryCheckFailedError < StandardError; end
|
||||||
|
class PrimaryCheckFailedError < StandardError; end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -21,6 +21,10 @@ module Shoulda
|
||||||
Gem::Requirement.new('>= 5').satisfied_by?(active_record_version)
|
Gem::Requirement.new('>= 5').satisfied_by?(active_record_version)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def active_record_gte_6?
|
||||||
|
Gem::Requirement.new('>= 6').satisfied_by?(active_record_version)
|
||||||
|
end
|
||||||
|
|
||||||
def active_record_version
|
def active_record_version
|
||||||
Gem::Version.new(::ActiveRecord::VERSION::STRING)
|
Gem::Version.new(::ActiveRecord::VERSION::STRING)
|
||||||
rescue NameError
|
rescue NameError
|
||||||
|
|
|
@ -25,7 +25,7 @@ module UnitTests
|
||||||
&block
|
&block
|
||||||
)
|
)
|
||||||
@table_name = table_name
|
@table_name = table_name
|
||||||
@columns = columns
|
@columns = normalize_columns(columns)
|
||||||
@connection = connection
|
@connection = connection
|
||||||
@customizer = block || proc {}
|
@customizer = block || proc {}
|
||||||
end
|
end
|
||||||
|
@ -66,6 +66,26 @@ module UnitTests
|
||||||
to: UnitTests::DatabaseHelpers,
|
to: UnitTests::DatabaseHelpers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def normalize_columns(columns)
|
||||||
|
if columns.is_a?(Hash)
|
||||||
|
if columns.values.first.is_a?(Hash)
|
||||||
|
columns
|
||||||
|
else
|
||||||
|
columns.transform_values do |value|
|
||||||
|
if value == false
|
||||||
|
value
|
||||||
|
else
|
||||||
|
{ type: value }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
columns.inject({}) do |hash, column_name|
|
||||||
|
hash.merge(column_name => { type: :string })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def add_columns_to_table(table)
|
def add_columns_to_table(table)
|
||||||
columns.each do |column_name, column_specification|
|
columns.each do |column_name, column_specification|
|
||||||
add_column_to_table(table, column_name, column_specification)
|
add_column_to_table(table, column_name, column_specification)
|
||||||
|
@ -75,39 +95,34 @@ module UnitTests
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_column_to_table(table, column_name, column_specification)
|
def add_column_to_table(table, column_name, column_specification)
|
||||||
if column_specification.is_a?(Hash)
|
column_specification = column_specification.dup
|
||||||
column_specification = column_specification.dup
|
column_type = column_specification.delete(:type)
|
||||||
column_type = column_specification.delete(:type)
|
column_options = column_specification.delete(:options) { {} }
|
||||||
column_options = column_specification.delete(:options) { {} }
|
|
||||||
|
|
||||||
if column_options[:array]
|
if column_options[:array]
|
||||||
if !active_record_supports_array_columns?
|
if !active_record_supports_array_columns?
|
||||||
raise ArgumentError.new(
|
|
||||||
'An array column is being added to a table, but this version ' +
|
|
||||||
"of ActiveRecord (#{active_record_version}) " +
|
|
||||||
'does not support array columns.',
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
if !database_supports_array_columns?
|
|
||||||
raise ArgumentError.new(
|
|
||||||
'An array column is being added to a table, but this ' +
|
|
||||||
"database adapter (#{database_adapter}) " +
|
|
||||||
'does not support array columns.',
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if column_specification.any?
|
|
||||||
raise ArgumentError.new(
|
raise ArgumentError.new(
|
||||||
"Invalid column specification.\nYou need to put " +
|
'An array column is being added to a table, but this version ' +
|
||||||
"#{column_specification.keys.map(&:inspect).to_sentence} " +
|
"of ActiveRecord (#{active_record_version}) " +
|
||||||
'inside an :options key!',
|
'does not support array columns.',
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
else
|
|
||||||
column_type = column_specification
|
if !database_supports_array_columns?
|
||||||
column_options = {}
|
raise ArgumentError.new(
|
||||||
|
'An array column is being added to a table, but this ' +
|
||||||
|
"database adapter (#{database_adapter}) " +
|
||||||
|
'does not support array columns.',
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if column_specification.any?
|
||||||
|
raise ArgumentError.new(
|
||||||
|
"Invalid column specification.\nYou need to put " +
|
||||||
|
"#{column_specification.keys.map(&:inspect).to_sentence} " +
|
||||||
|
'inside an :options key!',
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
table.column(column_name, column_type, column_options)
|
table.column(column_name, column_type, column_options)
|
||||||
|
|
|
@ -50,5 +50,9 @@ module UnitTests
|
||||||
def active_record_supports_validate_presence_on_active_storage?
|
def active_record_supports_validate_presence_on_active_storage?
|
||||||
active_record_version >= '6.0.0.beta1'
|
active_record_version >= '6.0.0.beta1'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def active_record_supports_implicit_order_column?
|
||||||
|
active_record_version >= '6.0.0.beta1'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,10 @@ module UnitTests
|
||||||
ModelBuilder.define_model(*args, &block)
|
ModelBuilder.define_model(*args, &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def define_model_instance(*args, &block)
|
||||||
|
define_model(*args, &block).new
|
||||||
|
end
|
||||||
|
|
||||||
def define_model_class(*args, &block)
|
def define_model_class(*args, &block)
|
||||||
ModelBuilder.define_model_class(*args, &block)
|
ModelBuilder.define_model_class(*args, &block)
|
||||||
end
|
end
|
||||||
|
|
|
@ -204,8 +204,7 @@ end
|
||||||
bundle.remove_gem 'byebug'
|
bundle.remove_gem 'byebug'
|
||||||
bundle.remove_gem 'web-console'
|
bundle.remove_gem 'web-console'
|
||||||
bundle.add_gem 'pg'
|
bundle.add_gem 'pg'
|
||||||
bundle.remove_gem 'sqlite3'
|
bundle.add_gem 'sqlite', '~> 1.3.6'
|
||||||
bundle.add_gem 'sqlite3', '~> 1.3.6'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,173 @@
|
||||||
|
require 'unit_spec_helper'
|
||||||
|
|
||||||
|
describe Shoulda::Matchers::ActiveRecord::HaveImplicitOrderColumnMatcher, type: :model do
|
||||||
|
if active_record_supports_implicit_order_column?
|
||||||
|
context 'when the given column exists' do
|
||||||
|
context 'when an implicit_order_column is set on the model' do
|
||||||
|
context 'and it matches the given column name' do
|
||||||
|
context 'and the column name is a symbol' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_with_implicit_order_column_on(
|
||||||
|
:created_at,
|
||||||
|
class_name: 'Employee',
|
||||||
|
columns: [:created_at]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { have_implicit_order_column(:created_at) }
|
||||||
|
.to match_against(record)
|
||||||
|
.or_fail_with(<<~MESSAGE, wrap: true)
|
||||||
|
Expected Employee not to have an implicit_order_column of
|
||||||
|
:created_at, but it did.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the column name is a string' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_with_implicit_order_column_on(
|
||||||
|
:created_at,
|
||||||
|
class_name: 'Employee',
|
||||||
|
columns: [:created_at]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { have_implicit_order_column('created_at') }
|
||||||
|
.to match_against(record)
|
||||||
|
.or_fail_with(<<~MESSAGE, wrap: true)
|
||||||
|
Expected Employee not to have an implicit_order_column of
|
||||||
|
:created_at, but it did.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and it does not match the given column name' do
|
||||||
|
context 'and the column name is a symbol' do
|
||||||
|
it 'does not match, producing an appropriate message' do
|
||||||
|
record = record_with_implicit_order_column_on(
|
||||||
|
:created_at,
|
||||||
|
class_name: 'Employee',
|
||||||
|
columns: [:created_at, :email]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { have_implicit_order_column(:email) }
|
||||||
|
.not_to match_against(record)
|
||||||
|
.and_fail_with(<<-MESSAGE, wrap: true)
|
||||||
|
Expected Employee to have an implicit_order_column of :email,
|
||||||
|
but it is :created_at.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the column name is a string' do
|
||||||
|
it 'does not match, producing an appropriate message' do
|
||||||
|
record = record_with_implicit_order_column_on(
|
||||||
|
:created_at,
|
||||||
|
class_name: 'Employee',
|
||||||
|
columns: [:created_at, :email]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { have_implicit_order_column('email') }
|
||||||
|
.not_to match_against(record)
|
||||||
|
.and_fail_with(<<-MESSAGE, wrap: true)
|
||||||
|
Expected Employee to have an implicit_order_column of :email,
|
||||||
|
but it is :created_at.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when no implicit_order_column is set on the model' do
|
||||||
|
context 'and the given column name is a symbol' do
|
||||||
|
it 'does not match, producing an appropriate message' do
|
||||||
|
record = record_without_implicit_order_column(
|
||||||
|
class_name: 'Employee',
|
||||||
|
columns: [:created_at]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { have_implicit_order_column(:created_at) }
|
||||||
|
.not_to match_against(record)
|
||||||
|
.and_fail_with(<<-MESSAGE, wrap: true)
|
||||||
|
Expected Employee to have an implicit_order_column of
|
||||||
|
:created_at, but implicit_order_column is not set.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the given column name is a string' do
|
||||||
|
it 'does not match, producing an appropriate message' do
|
||||||
|
record = record_without_implicit_order_column(
|
||||||
|
class_name: 'Employee',
|
||||||
|
columns: [:created_at]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { have_implicit_order_column('created_at') }
|
||||||
|
.not_to match_against(record)
|
||||||
|
.and_fail_with(<<-MESSAGE, wrap: true)
|
||||||
|
Expected Employee to have an implicit_order_column of
|
||||||
|
:created_at, but implicit_order_column is not set.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the given column does not exist' do
|
||||||
|
context 'and it is a symbol' do
|
||||||
|
it 'does not match, producing an appropriate message' do
|
||||||
|
record = record_without_any_columns(class_name: 'Employee')
|
||||||
|
|
||||||
|
expect { have_implicit_order_column(:whatever) }
|
||||||
|
.not_to match_against(record)
|
||||||
|
.and_fail_with(<<-MESSAGE, wrap: true)
|
||||||
|
Expected Employee to have an implicit_order_column of :whatever,
|
||||||
|
but that could not be proved: The :employees table does not have a
|
||||||
|
:whatever column.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and it is a string' do
|
||||||
|
it 'does not match, producing an appropriate message' do
|
||||||
|
record = record_without_any_columns(class_name: 'Employee')
|
||||||
|
|
||||||
|
expect { have_implicit_order_column('whatever') }
|
||||||
|
.not_to match_against(record)
|
||||||
|
.and_fail_with(<<-MESSAGE, wrap: true)
|
||||||
|
Expected Employee to have an implicit_order_column of :whatever,
|
||||||
|
but that could not be proved: The :employees table does not have a
|
||||||
|
:whatever column.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#description' do
|
||||||
|
it 'returns the correct description' do
|
||||||
|
matcher = have_implicit_order_column(:created_at)
|
||||||
|
|
||||||
|
expect(matcher.description).to eq(
|
||||||
|
'have an implicit_order_column of :created_at'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def record_with_implicit_order_column_on(
|
||||||
|
column_name,
|
||||||
|
class_name:,
|
||||||
|
columns: { column_name => :string }
|
||||||
|
)
|
||||||
|
define_model_instance(class_name, columns) do |model|
|
||||||
|
model.implicit_order_column = column_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def record_without_implicit_order_column(class_name:, columns:)
|
||||||
|
define_model_instance(class_name, columns)
|
||||||
|
end
|
||||||
|
|
||||||
|
def record_without_any_columns(class_name:)
|
||||||
|
define_model_instance(class_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue