Module: Radius::Spec::ModelFactory
- Defined in:
- lib/radius/spec/model_factory.rb
Overview
Basic Model Factory
This factory is not Rails specific. It works for any object type that
responds to new with a hash of attributes or keywords; including
Struct using the new Ruby 2.5 keyword_init flag.
To make this feature available require it after the gem:
require 'radius/spec'
require 'radius/spec/model_factory'
Storing Factory Templates
Our convention is to store all of a project's factory templates in the
file spec/support/model_factories.rb. As this is our convention, when
the model factory is required it will attempt to load this file
automatically as a convenience.
Including Helpers in Specs
There are multiple ways you can build object instances using this model factory. Which method you choose depends on how much perceived magic/syntactic sugar you want:
- call the model factory directly
- manually include the factory helper methods in the specs
- use metadata to auto load this feature and include it in the specs
When using the metadata option you do not need to explicitly require the
model factory feature. This gem registers metadata with the RSpec
configuration when it loads and RSpec is defined. When the metadata is
first used it will automatically require the model factory feature and
include the helpers.
Any of following metadata will include the factory helpers:
:model_factory:model_factoriestype: :controllertype: :featuretype: :jobtype: :modeltype: :requesttype: :system
Defined Under Namespace
Classes: TemplateNotFound
Class Method Summary collapse
-
.build(name, custom_attrs = {}, &block) ⇒ Object
Convenience wrapper for building a model template.
-
.catalog {|catalog| ... } ⇒ void
Suggested method for defining multiple factory templates at once.
-
.create(name, custom_attrs = {}, &block) ⇒ Object
Convenience wrapper for building, and persisting, a model template.
-
.define_factory(class_name, attrs = {}) ⇒ void
(also: factory)
Convenience helper for registering a template to the current catalog.
Class Method Details
.build(name, custom_attrs = {}, &block) ⇒ Object
Convenience wrapper for building a model template.
All custom_attrs values are provided as is to the class initializer
(i.e. they are not duplicate or modified in any way). When an
attribute exists in both the registered template and custom_attrs the
value in custom_attrs will be used. The custom_attrs may also
include new attributes not defined in the factory template.
Optional Block
The block is optional. When provided it is passed directly to new
when initializing the instance. This is to support the common Ruby
idiom of yielding self within initialize:
class AnyClass
def initialize(attrs = {})
# setup attrs
yield self if block_given?
end
end
Since Ruby always supports passing a block to a method, even if the method does not use the block, it's possible the block will not run if the class being instantiated does yield to it.
Also, while the common idiom is to yield self classes are
free to yield anything. You need to be aware of how the class normally
behaves when passing a block to new.
The examples below show different ways of interacting with the following domain model and registered factory template:
Radius::Spec::ModelFactory.factory "AnyClass",
simple_attr: "any value",
array_attr: %w[any value],
optional_attr: :optional,
dynamic_attr: -> { rand(0..100) }
class AnyClass
def initialize(**opts)
opts.each do |k, v|
public_send "#{k}=", v
end
yield self if block_given?
end
attr_accessor :array_attr, :dynamic_attr, :optional_attr, :simple_attr
end
407 408 409 410 411 412 413 414 415 416 417 418 419 |
# File 'lib/radius/spec/model_factory.rb', line 407 def build(name, custom_attrs = {}, &block) name = name.to_s template = ::Radius::Spec::ModelFactory.template(name) template_only = template.keys - custom_attrs.keys attrs = template.slice(*template_only) .delete_if { |_, v| :optional == v } .transform_values! { |v| ::Radius::Spec::ModelFactory.safe_transform(v) } .merge(custom_attrs) # TODO: Always yield to the provided block even if new doesn't ::Object.const_get(name).new(attrs, &block) end |
.catalog {|catalog| ... } ⇒ void
This method returns an undefined value.
Suggested method for defining multiple factory templates at once.
Most projects end up having many domain models which need factories defined. Having to reference the full module constant every time you want to define a factory is tedious. Use this to define all of your model templates within a block.
134 135 136 |
# File 'lib/radius/spec/model_factory.rb', line 134 def catalog yield self end |
.create(name, custom_attrs = {}, &block) ⇒ Object
Convenience wrapper for building, and persisting, a model template.
This is a thin wrapper around build(name, attrs).tap(&:save!). The
persistence message save! will only be called on objects which
respond to it.
Avoid for New Code
It is strongly suggested that you avoid using create for new code.
Instead be explicit about when and how objects are persisted. This
allows you to have fine grain control over how your data is setup.
We suggest that you create instances which need to be persisted before your specs using the following syntax:
let(:an_instance) { build("AnyClass") }
before do
an_instance.save!
end
Alternatively if you really want for the instance be lazy instantiated, and persisted, pass the appropriate persistence method as the block:
let(:an_instance) { build("AnyClass", &:save!) }
456 457 458 459 460 |
# File 'lib/radius/spec/model_factory.rb', line 456 def create(name, custom_attrs = {}, &block) instance = build(name, custom_attrs, &block) instance.save! if instance.respond_to?(:save!) instance end |
.define_factory(class_name, attrs = {}) ⇒ void Also known as: factory
This method returns an undefined value.
Convenience helper for registering a template to the current catalog.
Registers the class_name in the catalog mapped to the provided
attrs attribute template.
Lazy Class Loading
When testing in isolation we often don't want to wait a long time for a lot of unnecessary project/app code to load. With that in mind we want to keep loading the model factory and all factory templates as fast as possible. This mean not loading the associated project/app code at factory template definition time. This way if you only need one or two factories your remaining domain model code won't be loaded.
Lazy class loading occurs when you register factory template using a
string or symbol for the fully qualified class_name. The only
requirement for this feature is that the class must be loaded by the
project/app, or made available via some auto-loading mechanism, by
the time the first instance is built by the factory.
Template Attribute Keys
Attribute keys may be defined using either strings or symbols.
However, they will be stored internally as symbols. This means that
when an object instance is create using the factory the attribute
hash will be provided to new with symbol keys.
Dynamic Attribute Values (i.e. Generators)
Dynamic attributes values may be registered by providing a Proc for
the value. For any template attribute which has a Proc for a value
making an instance through the factory will send call to the proc
with no args.
This only applies to template values which are instances of
Proc. If you define a template value using another
object which responds to call that object will be set as
the built instance's attribute value without receiving
call.
While this is a powerful technique we suggest keeping it's use to a minimum. There's a lot of benefit to generative, mutation, and fuzzy testing. We just aren't convinced it should be the default when you generate unit / general integration test data.
Optional and Required attributes
Templates may use the special symbols :optional and :required as
a means of documenting attributes. These special symbols are meant as
descriptive placeholders for developers reading the factory
definition. Any template attribute with a value of :optional, which
is not overwritten by a custom value, will be removed just prior to
building a new instance.
Those attributes marked as :required will not be removed. Instead
the symbol :required will be set as the attribute's value if it
isn't overwritten by the custom data. This type of value is a benign
default meant to cause errors to provide a more helpful description
(i.e. this attribute is required).
For Rails projects, we suggest using :required for any association
that is necessary for the object to be valid. We do not recommend
attempting to generate default records within the factory as this can
lead to unexpected database state; and hide relevant information away
from the specs which may depend on it.
"Safe" Attribute Duplication
In an effort to help limit accidental state leak between instances the factory will duplicate all non-frozen template values prior to building the instance. Duplication is only applied to the values registered for the templates. Custom values provided when building the instance are not duplicated.
251 252 253 |
# File 'lib/radius/spec/model_factory.rb', line 251 def define_factory(class_name, attrs = {}) templates[class_name.to_s] = attrs.transform_keys(&:to_sym).freeze end |