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:
Yuriy Orlov 2019-09-12 17:05:13 +03:00 committed by Elliot Winkler
parent c956f6a44d
commit f82329a679
8 changed files with 338 additions and 32 deletions

View File

@ -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/have_db_column_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_rich_text_matcher"
require "shoulda/matchers/active_record/have_secure_token_matcher"

View File

@ -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

View File

@ -21,6 +21,10 @@ module Shoulda
Gem::Requirement.new('>= 5').satisfied_by?(active_record_version)
end
def active_record_gte_6?
Gem::Requirement.new('>= 6').satisfied_by?(active_record_version)
end
def active_record_version
Gem::Version.new(::ActiveRecord::VERSION::STRING)
rescue NameError

View File

@ -25,7 +25,7 @@ module UnitTests
&block
)
@table_name = table_name
@columns = columns
@columns = normalize_columns(columns)
@connection = connection
@customizer = block || proc {}
end
@ -66,6 +66,26 @@ module UnitTests
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)
columns.each do |column_name, column_specification|
add_column_to_table(table, column_name, column_specification)
@ -75,39 +95,34 @@ module UnitTests
end
def add_column_to_table(table, column_name, column_specification)
if column_specification.is_a?(Hash)
column_specification = column_specification.dup
column_type = column_specification.delete(:type)
column_options = column_specification.delete(:options) { {} }
column_specification = column_specification.dup
column_type = column_specification.delete(:type)
column_options = column_specification.delete(:options) { {} }
if column_options[:array]
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?
if column_options[:array]
if !active_record_supports_array_columns?
raise ArgumentError.new(
"Invalid column specification.\nYou need to put " +
"#{column_specification.keys.map(&:inspect).to_sentence} " +
'inside an :options key!',
'An array column is being added to a table, but this version ' +
"of ActiveRecord (#{active_record_version}) " +
'does not support array columns.',
)
end
else
column_type = column_specification
column_options = {}
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(
"Invalid column specification.\nYou need to put " +
"#{column_specification.keys.map(&:inspect).to_sentence} " +
'inside an :options key!',
)
end
table.column(column_name, column_type, column_options)

View File

@ -50,5 +50,9 @@ module UnitTests
def active_record_supports_validate_presence_on_active_storage?
active_record_version >= '6.0.0.beta1'
end
def active_record_supports_implicit_order_column?
active_record_version >= '6.0.0.beta1'
end
end
end

View File

@ -10,6 +10,10 @@ module UnitTests
ModelBuilder.define_model(*args, &block)
end
def define_model_instance(*args, &block)
define_model(*args, &block).new
end
def define_model_class(*args, &block)
ModelBuilder.define_model_class(*args, &block)
end

View File

@ -204,8 +204,7 @@ end
bundle.remove_gem 'byebug'
bundle.remove_gem 'web-console'
bundle.add_gem 'pg'
bundle.remove_gem 'sqlite3'
bundle.add_gem 'sqlite3', '~> 1.3.6'
bundle.add_gem 'sqlite', '~> 1.3.6'
end
end

View File

@ -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