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_factories
type: :controller
type: :feature
type: :job
type: :model
type: :request
type: :system
Defined Under Namespace
Classes: TemplateNotFound
Class Method Summary collapse
-
.build(name, custom_attrs = {}, &block) ⇒ Object
Convenience wrapper for building a model template.
-
.build!(name, custom_attrs = {}, &block) ⇒ Object
Convenience wrapper for building, and persisting, a model template.
-
.catalog {|catalog| ... } ⇒ void
Suggested method for defining multiple factory templates at once.
-
.create(name, custom_attrs = {}, &block) ⇒ Object
Legacy helper provided for backwards compatibility support.
-
.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
426 427 428 429 430 431 432 433 |
# File 'lib/radius/spec/model_factory.rb', line 426 def build(name, custom_attrs = {}, &block) name = name.to_s template = ::Radius::Spec::ModelFactory.template(name) custom_attrs = custom_attrs.transform_keys(&:to_sym) attrs = ::Radius::Spec::ModelFactory.merge_attrs(template, custom_attrs) # TODO: Always yield to the provided block even if new doesn't ::Object.const_get(name).new(attrs, &block) end |
.build!(name, custom_attrs = {}, &block) ⇒ Object
It is generally suggested that you avoid using build!
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
Convenience wrapper for building, and persisting, a model template.
This is a thin wrapper around:
build(name, attrs, &block).tap(&:save!)
The persistence message save!
will only be called on objects which
respond to it.
468 469 470 471 472 |
# File 'lib/radius/spec/model_factory.rb', line 468 def build!(name, custom_attrs = {}, &block) instance = build(name, custom_attrs, &block) instance.save! if instance.respond_to?(:save!) instance 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.
141 142 143 |
# File 'lib/radius/spec/model_factory.rb', line 141 def catalog yield self end |
.create(name, custom_attrs = {}, &block) ⇒ Object
Legacy helper provided for backwards compatibility support.
This provides the same behavior as build! and will be removed in a future release.
484 485 486 |
# File 'lib/radius/spec/model_factory.rb', line 484 def create(name, custom_attrs = {}, &block) build!(name, custom_attrs, &block) 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.
258 259 260 |
# File 'lib/radius/spec/model_factory.rb', line 258 def define_factory(class_name, attrs = {}) templates[class_name.to_s] = attrs.transform_keys(&:to_sym).freeze end |