Extract classes for defining models in tests

The main driver behind this commit is to provide a programmatic way to
define models in tests. We already have ways of doing this, of course,
with `define_model` and `define_active_model_class`, but these methods
are very low-level, and in writing tests we have historically made our
own methods inside of test files to define full and complete models. So
we have this common pattern of defining a model with a validation, and
that's repeated across many different files.

What we would like to do, right now, is extract some commonly used
assertions to a shared example group. These assertions need to define
models inside of the tests, but the issue is that sometimes the models
are ActiveRecord models, and sometimes they are ActiveModel models, and
when the shared example group is used within a test file, we need a way
to choose the strategy we'd like to use at runtime. Since the way we
currently define models is via methods, we can't really provide a
strategy very easily. Also, if we need to customize how those models are
defined (say, the attribute needs to be a has-many association instead
of a normal attribute) then the methods only go so far in providing us
that level of customization before things get really complicated.

So, to help us with this, this commit takes the pattern of
model-plus-validation previously mentioned and places it in multiple
classes.

Note that this is also a precursor to a later commit in which we
introduce `ignoring_interference_by_writer` across the board. The way we
will do this is by adding a shared example group that then uses these
model creation classes internally to build objects instead of relying
upon methods that the outer example group -- to which the shared example
group is being mixed into -- provides.
This commit is contained in:
Elliot Winkler 2015-12-23 18:10:40 -05:00
parent 67d5189437
commit 85a3b03c30
20 changed files with 1194 additions and 164 deletions

View File

@ -0,0 +1,61 @@
module UnitTests
module ActiveRecord
class CreateTable
def self.call(table_name, columns)
new(table_name, columns).call
end
def initialize(table_name, columns)
@table_name = table_name
@columns = columns
end
def call
if columns.key?(:id) && columns[:id] == false
columns.delete(:id)
UnitTests::ModelBuilder.create_table(
table_name,
id: false,
&method(:add_columns_to_table)
)
else
UnitTests::ModelBuilder.create_table(
table_name,
&method(:add_columns_to_table)
)
end
end
protected
attr_reader :table_name, :columns
private
def add_columns_to_table(table)
columns.each do |column_name, column_specification|
add_column_to_table(table, column_name, column_specification)
end
end
def add_column_to_table(table, column_name, column_specification)
if column_specification.is_a?(Hash)
column_type = column_specification.fetch(:type)
column_options = column_specification.fetch(:options, {})
else
column_type = column_specification
column_options = {}
end
begin
table.__send__(column_type, column_name, column_options)
rescue NoMethodError
raise ColumnNotSupportedError.new(
"#{Tests::Database.instance.adapter_class} does not support " +
":#{column_type} columns."
)
end
end
end
end
end

View File

@ -0,0 +1,47 @@
module UnitTests
class Attribute
attr_reader :name, :column_type, :column_options
DEFAULT_COLUMN_TYPE = :string
DEFAULT_COLUMN_OPTIONS = {
null: false,
array: false
}
def initialize(args)
@args = args
end
def name
args.fetch(:name)
end
def column_type
args.fetch(:column_type, DEFAULT_COLUMN_TYPE)
end
def column_options
DEFAULT_COLUMN_OPTIONS.
merge(args.fetch(:column_options, {})).
merge(type: column_type)
end
def array?
column_options[:array]
end
def default_value
args.fetch(:default_value) do
if column_options[:null]
nil
else
Shoulda::Matchers::Util.dummy_value_for(value_type, array: array?)
end
end
end
protected
attr_reader :args
end
end

View File

@ -0,0 +1,111 @@
module UnitTests
class ChangeValue
def self.call(column_type, value, value_changer)
new(column_type, value, value_changer).call
end
def initialize(column_type, value, value_changer)
@column_type = column_type
@value = value
@value_changer = value_changer
end
def call
if value_changer.is_a?(Proc)
value_changer.call(value)
elsif respond_to?(value_changer, true)
send(value_changer)
else
value.public_send(value_changer)
end
end
protected
attr_reader :column_type, :value, :value_changer
private
def previous_value
if value.is_a?(String)
value[0..-2] + (value[-1].ord - 1).chr
else
value - 1
end
end
def next_value
if value.is_a?(Array)
value + [value.first.class.new]
elsif value.respond_to?(:next)
value.next
else
value + 1
end
end
def next_next_value
change_value(change_value(value, :next_value), :next_value)
end
def next_value_or_numeric_value
if value
change_value(value, :next_value)
else
change_value(value, :numeric_value)
end
end
def next_value_or_non_numeric_value
if value
change_value(value, :next_value)
else
change_value(value, :non_numeric_value)
end
end
def never_falsy
value || dummy_value_for_column
end
def truthy_or_numeric
value || 1
end
def never_blank
value.presence || dummy_value_for_column
end
def nil_to_blank
value || ''
end
def always_nil
nil
end
def add_character
value + 'a'
end
def remove_character
value.chop
end
def numeric_value
'1'
end
def non_numeric_value
'a'
end
def change_value(value, value_changer)
self.class.call(column_type, value, value_changer)
end
def dummy_value_for_column
Shoulda::Matchers::Util.dummy_value_for(column_type)
end
end
end

View File

@ -0,0 +1,135 @@
require 'forwardable'
module UnitTests
module CreateModelArguments
class Basic
DEFAULT_MODEL_NAME = 'Example'
DEFAULT_ATTRIBUTE_NAME = :attr
DEFAULT_COLUMN_TYPE = :string
def self.wrap(args)
if args.is_a?(self)
args
else
new(args)
end
end
extend Forwardable
def_delegators(
:attribute,
:column_type,
:column_options,
:default_value,
:value_type
)
def initialize(args)
@args = args
end
def fetch(*args, &block)
self.args.fetch(*args, &block)
end
def merge(given_args)
self.class.new(args.deep_merge(given_args))
end
def model_name
args.fetch(:model_name, DEFAULT_MODEL_NAME)
end
def attribute_name
args.fetch(:attribute_name, default_attribute_name)
end
def model_creation_strategy
args.fetch(:model_creation_strategy)
end
def columns
{ attribute_name => column_options }
end
def attribute
@_attribute ||= attribute_class.new(attribute_args)
end
def all_attribute_overrides
@_all_attribute_overrides ||= begin
attribute_overrides = args.slice(
:changing_values_with,
:default_value
)
overrides =
if attribute_overrides.empty?
{}
else
{ attribute_name => attribute_overrides }
end
overrides.deep_merge(args.fetch(:attribute_overrides, {}))
end
end
def attribute_overrides
all_attribute_overrides.fetch(attribute_name, {})
end
def validation_name
args.fetch(:validation_name) { map_matcher_name_to_validation_name }
end
def validation_options
args.fetch(:validation_options, {})
end
def custom_validation?
args.fetch(:custom_validation, false)
end
def matcher_name
args.fetch(:matcher_name)
end
def attribute_default_values_by_name
if attribute_overrides.key?(:default_value)
{ attribute_name => attribute_overrides[:default_value] }
else
{}
end
end
def to_hash
args.deep_dup
end
protected
attr_reader :args
def attribute_class
UnitTests::Attribute
end
def default_attribute_name
DEFAULT_ATTRIBUTE_NAME
end
private
def map_matcher_name_to_validation_name
matcher_name.to_s.sub('validate', 'validates')
end
def attribute_args
args.slice(:column_type).deep_merge(
attribute_overrides.deep_merge(name: attribute_name)
)
end
end
end
end

View File

@ -0,0 +1,15 @@
module UnitTests
module CreateModelArguments
class HasMany < Basic
def columns
super.except(attribute_name)
end
private
def default_attribute_name
:children
end
end
end
end

View File

@ -0,0 +1,74 @@
module UnitTests
module CreateModelArguments
class UniquenessMatcher < Basic
def self.normalize_attribute(attribute)
if attribute.is_a?(Hash)
Attribute.new(attribute)
else
Attribute.new(name: attribute)
end
end
def self.normalize_attributes(attributes)
attributes.map do |attribute|
normalize_attribute(attribute)
end
end
def columns
attributes.reduce({}) do |options, attribute|
options.merge(
attribute.name => {
type: attribute.column_type,
options: attribute.column_options
}
)
end
end
def validation_options
super.merge(scope: scope_attribute_names)
end
def attribute_default_values_by_name
attributes.reduce({}) do |values, attribute|
values.merge(attribute.name => attribute.default_value)
end
end
protected
def attribute_class
Attribute
end
private
def attributes
[attribute] + scope_attributes + additional_attributes
end
def scope_attribute_names
scope_attributes.map(&:name)
end
def scope_attributes
@_scope_attributes ||= self.class.normalize_attributes(
args.fetch(:scopes, [])
)
end
def additional_attributes
@_additional_attributes ||= self.class.normalize_attributes(
args.fetch(:additional_attributes, [])
)
end
class Attribute < UnitTests::Attribute
def value_type
args.fetch(:value_type) { column_type }
end
end
end
end
end

View File

@ -6,7 +6,7 @@ module UnitTests
end
def active_record_version
Tests::Version.new(ActiveRecord::VERSION::STRING)
Tests::Version.new(::ActiveRecord::VERSION::STRING)
end
def active_record_supports_enum?

View File

@ -1,76 +1,90 @@
module UnitTests
module ClassBuilder
def self.parse_constant_name(name)
namespace = Shoulda::Matchers::Util.deconstantize(name)
qualified_namespace = (namespace.presence || 'Object').constantize
name_without_namespace = name.to_s.demodulize
[qualified_namespace, name_without_namespace]
def define_module(*args, &block)
ClassBuilder.define_module(*args, &block)
end
def self.configure_example_group(example_group)
example_group.include(self)
example_group.after do
teardown_defined_constants
end
def define_class(*args, &block)
ClassBuilder.define_class(*args, &block)
end
def define_module(module_name, &block)
module_name = module_name.to_s.camelize
class << self
def configure_example_group(example_group)
example_group.include(self)
namespace, name_without_namespace =
ClassBuilder.parse_constant_name(module_name)
if namespace.const_defined?(name_without_namespace, false)
namespace.__send__(:remove_const, name_without_namespace)
end
eval <<-RUBY
module #{namespace}::#{name_without_namespace}
end
RUBY
namespace.const_get(name_without_namespace).tap do |constant|
constant.unloadable
if block
constant.module_eval(&block)
example_group.after do
ClassBuilder.reset
end
end
end
def define_class(class_name, parent_class = Object, &block)
class_name = class_name.to_s.camelize
namespace, name_without_namespace =
ClassBuilder.parse_constant_name(class_name)
if namespace.const_defined?(name_without_namespace, false)
namespace.__send__(:remove_const, name_without_namespace)
def reset
remove_defined_classes
end
eval <<-RUBY
class #{namespace}::#{name_without_namespace} < #{parent_class}
def define_module(module_name, &block)
module_name = module_name.to_s.camelize
namespace, name_without_namespace =
ClassBuilder.parse_constant_name(module_name)
if namespace.const_defined?(name_without_namespace, false)
namespace.__send__(:remove_const, name_without_namespace)
end
RUBY
namespace.const_get(name_without_namespace).tap do |constant|
constant.unloadable
eval <<-RUBY
module #{namespace}::#{name_without_namespace}
end
RUBY
if block
if block.arity == 0
constant.class_eval(&block)
else
block.call(constant)
namespace.const_get(name_without_namespace).tap do |constant|
constant.unloadable
if block
constant.module_eval(&block)
end
end
end
end
private
def define_class(class_name, parent_class = Object, &block)
class_name = class_name.to_s.camelize
def teardown_defined_constants
ActiveSupport::Dependencies.clear
namespace, name_without_namespace =
ClassBuilder.parse_constant_name(class_name)
if namespace.const_defined?(name_without_namespace, false)
namespace.__send__(:remove_const, name_without_namespace)
end
eval <<-RUBY
class #{namespace}::#{name_without_namespace} < ::#{parent_class}
end
RUBY
namespace.const_get(name_without_namespace).tap do |constant|
constant.unloadable
if block
if block.arity == 0
constant.class_eval(&block)
else
block.call(constant)
end
end
end
end
def parse_constant_name(name)
namespace = Shoulda::Matchers::Util.deconstantize(name)
qualified_namespace = (namespace.presence || 'Object').constantize
name_without_namespace = name.to_s.demodulize
[qualified_namespace, name_without_namespace]
end
private
def remove_defined_classes
::ActiveSupport::Dependencies.clear
end
end
end
end

View File

@ -2,136 +2,113 @@ require_relative 'class_builder'
module UnitTests
module ModelBuilder
include ClassBuilder
def create_table(*args, &block)
ModelBuilder.create_table(*args, &block)
end
def self.configure_example_group(example_group)
example_group.include(self)
def define_model(*args, &block)
ModelBuilder.define_model(*args, &block)
end
example_group.after do
def define_model_class(*args, &block)
ModelBuilder.define_model_class(*args, &block)
end
def define_active_model_class(*args, &block)
ModelBuilder.define_active_model_class(*args, &block)
end
class << self
def configure_example_group(example_group)
example_group.include(self)
example_group.after do
ModelBuilder.reset
end
end
def reset
clear_column_caches
drop_created_tables
created_tables.clear
defined_models.clear
end
end
def create_table(table_name, options = {}, &block)
connection = ActiveRecord::Base.connection
def create_table(table_name, options = {}, &block)
connection = ::ActiveRecord::Base.connection
begin
connection.execute("DROP TABLE IF EXISTS #{table_name}")
connection.create_table(table_name, options, &block)
created_tables << table_name
connection
rescue Exception => e
connection.execute("DROP TABLE IF EXISTS #{table_name}")
raise e
end
end
def define_model_class(class_name, &block)
define_class(class_name, ActiveRecord::Base, &block)
end
def define_active_model_class(class_name, options = {}, &block)
accessors = options.fetch(:accessors, [])
attributes_module = Module.new do
accessors.each do |column|
attr_accessor column.to_sym
begin
connection.execute("DROP TABLE IF EXISTS #{table_name}")
connection.create_table(table_name, options, &block)
created_tables << table_name
connection
rescue Exception => e
connection.execute("DROP TABLE IF EXISTS #{table_name}")
raise e
end
end
define_class(class_name) do
include ActiveModel::Validations
include attributes_module
def initialize(attributes = {})
attributes.each do |name, value|
__send__("#{name}=", value)
end
end
if block_given?
class_eval(&block)
end
def define_model_class(class_name, &block)
ClassBuilder.define_class(class_name, ::ActiveRecord::Base, &block)
end
end
def define_model(name, columns = {}, &block)
class_name = name.to_s.pluralize.classify
table_name = class_name.tableize.gsub('/', '_')
def define_active_model_class(class_name, options = {}, &block)
attribute_names = options.delete(:accessors) { [] }
table_block = lambda do |table|
columns.each do |column_name, specification|
if specification.is_a?(Hash)
column_type = specification[:type]
column_options = specification.fetch(:options, {})
else
column_type = specification
column_options = {}
end
columns = attribute_names.reduce({}) do |hash, attribute_name|
hash.merge(attribute_name => nil)
end
begin
table.__send__(column_type, column_name, column_options)
rescue NoMethodError
raise NoMethodError, "#{Tests::Database.instance.adapter_class} does not support :#{column_type} columns"
end
UnitTests::ModelCreationStrategies::ActiveModel.call(
'Example',
columns,
options,
&block
)
end
def define_model(name, columns = {}, options = {}, &block)
model = UnitTests::ModelCreationStrategies::ActiveRecord.call(
name,
columns,
options,
&block
)
defined_models << model
model
end
private
def clear_column_caches
# Rails 4.x
if ::ActiveRecord::Base.connection.respond_to?(:schema_cache)
::ActiveRecord::Base.connection.schema_cache.clear!
# Rails 3.1 - 4.0
elsif ::ActiveRecord::Base.connection_pool.respond_to?(:clear_cache!)
::ActiveRecord::Base.connection_pool.clear_cache!
end
defined_models.each do |model|
model.reset_column_information
end
end
if columns.key?(:id) && columns[:id] == false
columns.delete(:id)
create_table(table_name, id: false, &table_block)
else
create_table(table_name, &table_block)
end
def drop_created_tables
connection = ::ActiveRecord::Base.connection
model = define_model_class(class_name).tap do |m|
if block
if block.arity == 0
m.class_eval(&block)
else
block.call(m)
end
created_tables.each do |table_name|
connection.execute("DROP TABLE IF EXISTS #{table_name}")
end
m.table_name = table_name
end
defined_models << model
model
end
private
def clear_column_caches
# Rails 4.x
if ActiveRecord::Base.connection.respond_to?(:schema_cache)
ActiveRecord::Base.connection.schema_cache.clear!
# Rails 3.1 - 4.0
elsif ActiveRecord::Base.connection_pool.respond_to?(:clear_cache!)
ActiveRecord::Base.connection_pool.clear_cache!
def created_tables
@_created_tables ||= []
end
defined_models.each do |model|
model.reset_column_information
def defined_models
@_defined_models ||= []
end
end
def drop_created_tables
connection = ActiveRecord::Base.connection
created_tables.each do |table_name|
connection.execute("DROP TABLE IF EXISTS #{table_name}")
end
end
def created_tables
@_created_tables ||= []
end
def defined_models
@_defined_models ||= []
end
end
end

View File

@ -0,0 +1,111 @@
module UnitTests
module ModelCreationStrategies
class ActiveModel
def self.call(name, columns = {}, options = {}, &block)
new(name, columns, options, &block).call
end
def initialize(name, columns = {}, options = {}, &block)
@name = name
@columns = columns
@options = options
@model_customizers = []
if block
customize_model(&block)
end
end
def customize_model(&block)
model_customizers << block
end
def call
ClassBuilder.define_class(name, Model).tap do |model|
model.columns = columns.keys
model_customizers.each do |block|
run_block(model, block)
end
end
end
protected
attr_reader :name, :columns, :model_customizers, :options
private
def run_block(model, block)
if block
if block.arity == 0
model.class_eval(&block)
else
block.call(model)
end
end
end
class Model
class << self
def columns
@_columns ||= []
end
def columns=(columns)
existing_columns = self.columns
new_columns = columns - existing_columns
@_columns += new_columns
include attributes_module
attributes_module.module_eval do
new_columns.each do |column|
define_method(column) do
attributes[column]
end
define_method("#{column}=") do |value|
attributes[column] = value
end
end
end
end
private
def attributes_module
@_attributes_module ||= Module.new
end
end
include ::ActiveModel::Model
attr_reader :attributes
def initialize(attributes = {})
@attributes = attributes.symbolize_keys
end
def inspect
middle = '%s:0x%014x%s' % [
self.class,
object_id * 2,
' ' + inspected_attributes.join(' ')
]
"#<#{middle.strip}>"
end
private
def inspected_attributes
self.class.columns.map do |key, value|
"#{key}: #{attributes[key].inspect}"
end
end
end
end
end
end

View File

@ -0,0 +1,79 @@
module UnitTests
module ModelCreationStrategies
class ActiveRecord
def self.call(name, columns = {}, options = {}, &block)
new(name, columns, options, &block).call
end
def initialize(name, columns = {}, options = {}, &block)
@name = name
@columns = columns
@options = options
@model_customizers = []
if block
customize_model(&block)
end
end
def customize_model(&block)
model_customizers << block
end
def call
create_table_for_model
define_class_for_model
end
protected
attr_reader :columns, :model_customizers, :name, :options
private
def create_table_for_model
UnitTests::ActiveRecord::CreateTable.call(table_name, columns)
end
def define_class_for_model
model = UnitTests::ModelBuilder.define_model_class(class_name)
model_customizers.each do |block|
run_block(model, block)
end
if whitelist_attributes?
model.attr_accessible(*columns.keys)
end
model.table_name = table_name
model
end
def run_block(model, block)
if block
if block.arity == 0
model.class_eval(&block)
else
block.call(model)
end
end
end
def class_name
name.to_s.pluralize.classify
end
def table_name
class_name.tableize.gsub('/', '_')
end
def whitelist_attributes?
options.fetch(:whitelist_attributes, true)
end
ColumnNotSupportedError = Class.new(StandardError)
end
end
end

View File

@ -0,0 +1,19 @@
module UnitTests
module ModelCreators
class << self
def register(name, klass)
registrations[name] = klass
end
def retrieve(name)
registrations[name]
end
private
def registrations
@_registrations ||= {}
end
end
end
end

View File

@ -0,0 +1,39 @@
require_relative '../model_creators'
require 'forwardable'
module UnitTests
module ModelCreators
class ActiveModel
def self.call(args)
new(args).call
end
extend Forwardable
def_delegators(
:arguments,
:attribute_name,
:attribute_default_values_by_name,
)
def initialize(args)
@arguments = CreateModelArguments::Basic.wrap(
args.merge(
model_creation_strategy: UnitTests::ModelCreationStrategies::ActiveModel
)
)
@model_creator = Basic.new(arguments)
end
def call
model_creator.call
end
protected
attr_reader :arguments, :model_creator
end
register(:active_model, ActiveModel)
end
end

View File

@ -0,0 +1,43 @@
require_relative '../model_creators'
require 'forwardable'
module UnitTests
module ModelCreators
class ActiveRecord
def self.call(args)
new(args).call
end
extend Forwardable
def_delegators(
:arguments,
:attribute_default_values_by_name,
:attribute_name,
:customize_model,
:model_name,
)
def_delegators :model_creator, :customize_model
def initialize(args)
@arguments = CreateModelArguments::Basic.wrap(
args.merge(
model_creation_strategy: UnitTests::ModelCreationStrategies::ActiveRecord
)
)
@model_creator = Basic.new(arguments)
end
def call
model_creator.call
end
protected
attr_reader :arguments, :model_creator
end
register(:active_record, ActiveRecord)
end
end

View File

@ -0,0 +1,95 @@
require_relative '../../model_creators'
require 'forwardable'
module UnitTests
module ModelCreators
class ActiveRecord
class HasAndBelongsToMany
def self.call(args)
new(args).call
end
extend Forwardable
def_delegators(
:arguments,
:attribute_name,
:attribute_default_values_by_name,
)
def initialize(args)
@arguments = CreateModelArguments::HasMany.wrap(args)
end
def call
parent_child_table_creator.call
child_model_creator.call
parent_model_creator.call
end
protected
attr_reader :arguments
private
alias_method :association_name, :attribute_name
alias_method :parent_model_creator_arguments, :arguments
def parent_child_table_creator
@_parent_child_table_creator ||=
UnitTests::ActiveRecord::CreateTable.new(
parent_child_table_name,
foreign_key_for_child_model => :integer,
foreign_key_for_parent_model => :integer,
:id => false
)
end
def child_model_creator
@_child_model_creator ||=
UnitTests::ModelCreationStrategies::ActiveRecord.new(
child_model_name
)
end
def parent_model_creator
@_parent_model_creator ||= begin
model_creator = UnitTests::ModelCreators::ActiveRecord.new(
parent_model_creator_arguments
)
# TODO: doesn't this need to be a has_many :through?
model_creator.customize_model do |model|
model.has_many(association_name)
end
model_creator
end
end
def foreign_key_for_child_model
child_model_name.foreign_key
end
def foreign_key_for_parent_model
parent_model_name.foreign_key
end
def parent_child_table_name
"#{child_model_name.pluralize}#{parent_model_name}".tableize
end
def parent_model_name
parent_model_creator.model_name
end
def child_model_name
association_name.to_s.classify
end
end
end
register(:"active_record/habtm", ActiveRecord::HasAndBelongsToMany)
end
end

View File

@ -0,0 +1,67 @@
require_relative '../../model_creators'
require 'forwardable'
module UnitTests
module ModelCreators
class ActiveRecord
class HasMany
def self.call(args)
new(args).call
end
extend Forwardable
def_delegators(
:arguments,
:attribute_name,
:attribute_default_values_by_name,
)
def initialize(args)
@arguments = CreateModelArguments::HasMany.wrap(args)
end
def call
child_model_creator.call
parent_model_creator.call
end
protected
attr_reader :arguments
private
alias_method :association_name, :attribute_name
alias_method :parent_model_creator_arguments, :arguments
def child_model_creator
@_child_model_creator ||=
UnitTests::ModelCreationStrategies::ActiveRecord.new(
child_model_name
)
end
def parent_model_creator
@_parent_model_creator ||= begin
model_creator = UnitTests::ModelCreators::ActiveRecord.new(
parent_model_creator_arguments
)
model_creator.customize_model do |model|
model.has_many(association_name)
end
model_creator
end
end
def child_model_name
association_name.to_s.classify
end
end
end
register(:"active_record/has_many", ActiveRecord::HasMany)
end
end

View File

@ -0,0 +1,42 @@
require_relative '../../model_creators'
require 'forwardable'
module UnitTests
module ModelCreators
class ActiveRecord
class UniquenessMatcher
def self.call(args)
new(args).call
end
extend Forwardable
def_delegators(
:arguments,
:attribute_name,
:attribute_default_values_by_name,
)
def initialize(args)
@arguments = CreateModelArguments::UniquenessMatcher.wrap(args)
@model_creator = UnitTests::ModelCreators::ActiveRecord.new(
arguments
)
end
def call
model_creator.call
end
protected
attr_reader :arguments, :model_creator
end
end
register(
:"active_record/uniqueness_matcher",
ActiveRecord::UniquenessMatcher
)
end
end

View File

@ -0,0 +1,97 @@
require 'forwardable'
module UnitTests
module ModelCreators
class Basic
def self.call(args)
new(args).call
end
extend Forwardable
def_delegators :arguments, :attribute_name, :model_name
def_delegators :model_creator, :customize_model
def initialize(arguments)
@arguments = arguments
@model_creator = build_model_creator
end
def call
model_creator.call
end
protected
attr_reader :arguments, :model_creator
private
def_delegators(
:arguments,
:additional_model_creation_strategy_args,
:all_attribute_overrides,
:columns,
:custom_validation?,
:model_creation_strategy,
:validation_name,
:validation_options,
:column_type,
)
def build_model_creator
model_creator = model_creation_strategy.new(
model_name,
columns,
arguments
)
model_creator.customize_model do |model|
add_validation_to(model)
possibly_override_attribute_writer_method_for(model)
end
model_creator
end
def add_validation_to(model)
if custom_validation?
_attribute_name = attribute_name
model.send(:define_method, :custom_validation) do
custom_validation.call(self, _attribute_name)
end
model.validate(:custom_validation)
else
model.public_send(validation_name, attribute_name, validation_options)
end
end
def possibly_override_attribute_writer_method_for(model)
all_attribute_overrides.each do |attribute_name, overrides|
if overrides.key?(:changing_values_with)
_change_value = method(:change_value)
model.send(:define_method, "#{attribute_name}=") do |value|
new_value = _change_value.call(
value,
overrides[:changing_values_with]
)
if respond_to?(:write_attribute)
write_attribute(new_value)
else
super(new_value)
end
end
end
end
end
def change_value(value, value_changer)
UnitTests::ChangeValue.call(column_type, value, value_changer)
end
end
end
end

View File

@ -108,4 +108,8 @@ describe Shoulda::Matchers::ActiveModel::AllowMassAssignmentOfMatcher, type: :mo
end
end
end
def define_model(name, columns, &block)
super(name, columns, whitelist_attributes: false, &block)
end
end

View File

@ -433,11 +433,11 @@ invalid and to raise a validation exception with message matching
context 'when the attribute cannot be changed from non-nil to nil' do
it 'raises an AttributeChangedValueError' do
model = define_active_model_class 'Example' do
attr_reader :name
model = define_active_model_class 'Example', accessors: [:name] do
def name=(value)
@name = value unless value.nil?
unless value.nil?
super(value)
end
end
end