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:
parent
67d5189437
commit
85a3b03c30
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue