Skip to content

Instantly share code, notes, and snippets.

@joegaudet
Forked from jilucev/composition_example.rb
Last active June 13, 2018 18:45
Show Gist options
  • Save joegaudet/fd011c3cc037e28d4d5ffa964721c9c8 to your computer and use it in GitHub Desktop.
Save joegaudet/fd011c3cc037e28d4d5ffa964721c9c8 to your computer and use it in GitHub Desktop.
module DependencySupport
def self.included(base)
base.extend(ClassMethods)
end
# Substitutes a give dependency with a provided value
# will raise an exception if a provided dependency is missing
# @param [Sumbol] dependency_name
# @param [*] value
# @param [DependencySupport] instance of dependant to allow for chaining
def substitute_dependency(dependency_name, dependency_value)
legal_dep = self.class.dependencies.any? {|dep| dep == dependency_name}
unless legal_dep
raise "'#{dependency_name}' is not a declared dependency of #{self.class.to_s}"
end
instance_variable_set("@__#{dependency_name}".to_sym, dependency_value)
self
end
# Substitutes a hash of dependencies with the provided dependencies
# will raise an exception if a provided dependency is missing
# @param [Object] dependencies
# @param [DependencySupport] instance of dependant to allow for chaining
def substitute(dependencies = {})
dependencies.each do |dependency_name, dependency_value|
substitute_dependency(dependency_name, dependency_value)
end
self
end
module ClassMethods
def substitute(dependencies = {})
self.new.substitute(dependencies)
end
# Defines a new dependency
# @param [Symbol] dependency_name
# @param [Class] dependency_class
# @param [Object] options
# @!macro [attach] dependency
# @return [$2] the $1 $0
def dependency(dependency_name, dependency_class, options = {})
dependencies.push(dependency_name)
instance = options[:instance]
dependency_variable_name = "@__#{dependency_name}".to_sym
dependency_initializer = options[:initializer]
if options[:static] && instance.present?
raise 'You cannot declare a static dependency and then provide an instance'
end
if options[:static] && dependency_initializer
raise 'You cannot declare a static dependency and then provide a custom initializer'
end
if dependency_initializer && instance.present?
raise 'You cannot declare a dependency_initializer and provide an instance'
end
if dependency_initializer && dependency_initializer.arity != 1
raise 'A custom initializer may only have 1 argument'
end
define_method(dependency_name) do
ret = instance_variable_get(dependency_variable_name)
if ret
ret
else
if dependency_initializer.nil?
# We are a standard initializer
if instance.present? ||
dependency_class.ancestors.include?(ActiveRecord::Base) ||
dependency_class.ancestors.include?(ActiveModel::Model) ||
dependency_class.ancestors.include?(ActionMailer::Base) ||
options[:static]
dependency_instance = instance || dependency_class
else
dependency_instance = dependency_class.new
end
else
# User overridden initializer
begin
dependency_instance = dependency_initializer.(self)
rescue NoMethodError => e
if e.message.include?(self.to_s)
raise "There was an error executing your custom initializer for '#{dependency_name}'"
else
raise e
end
end
end
instance_variable_set(dependency_variable_name, dependency_instance)
end
end
end
# @return [Array<Object>]
def dependencies
@dependencies ||= []
end
end
end
class DependencySupportTest < MiniTest::Test
describe DependencySupport do
class ARBaz < ActiveRecord::Base
end
class Bar
end
class Qux
attr_accessor :bar
def initialize(bar)
@bar = bar
end
end
class Static
def self.instance_bar
@instance_bar ||= Bar.new
end
end
class Foo
include DependencySupport
dependency :bar, Bar
dependency :static_bar, Bar, static: true
dependency :ar_baz_dao, ARBaz
dependency :instance_bar, Bar, instance: Static.instance_bar
dependency :qux, Qux, initializer: ->(receiver) {Qux.new(receiver.bar)}
end
it 'includes DependencySupport' do
assert Foo.ancestors.include?(DependencySupport)
end
it 'creates a dependency accessor for a class by initializing it' do
foo = Foo.new
bar = foo.bar
assert bar.is_a?(Bar)
end
it 'creates a dependency accessor for a AR Class' do
foo = Foo.new
baz_dao = foo.ar_baz_dao
assert_equal baz_dao, ARBaz
end
it 'creates a dependency accessor for instance references' do
foo = Foo.new
assert_equal foo.instance_bar, Static.instance_bar
end
it 'initializes dependencies using a provided custom initializer' do
foo = Foo.new
assert_equal foo.qux.bar, foo.bar
end
it 'creates static accessor when the static option is set' do
foo = Foo.new
assert_equal foo.static_bar, Bar
end
it 'allows for the substitution of dependencies' do
foo = Foo.new
foo.substitute(bar: 1)
assert_equal foo.bar, 1
foo.substitute_dependency(:bar, 2)
assert_equal foo.bar, 2
end
it 'raises an exception when you try and substitute a non existent dependency' do
foo = Foo.new
err = assert_raises do
foo.substitute(joe: 1)
end
assert_equal err.message, "'joe' is not a declared dependency of DependencySupportTest::Foo"
end
it 'raises an exception if the initializer fails' do
err = assert_raises do
class FailingFoo
include DependencySupport
dependency :qux, Qux, initializer: ->(receiver) {
Qux.new(receiver.bazo)
}
dependency :bar, Bar
end
FailingFoo.new.qux
end
assert_equal err.message, "There was an error executing your custom initializer for 'qux'"
end
it 'raises an exception if the initializer has the wrong number of arguments' do
err = assert_raises do
class FailingFoo2
include DependencySupport
dependency :qux, Qux, initializer: ->() {Qux.new(receiver.bar)}
end
end
assert_equal err.message, 'A custom initializer may only have 1 argument'
end
it 'raises an exception when you declare both a static dependency and provide an instance' do
err = assert_raises do
class FailingFoo3
include DependencySupport
dependency :qux, Qux, static: true, instance: Static.instance_bar
end
end
assert_equal err.message, 'You cannot declare a static dependency and then provide an instance'
end
it 'raises an exception when you declare both a static dependency and provide a custom initializer' do
err = assert_raises do
class FailingFoo4
include DependencySupport
dependency :qux, Qux, static: true, initializer: ->(foo) {}
end
end
assert_equal err.message, 'You cannot declare a static dependency and then provide a custom initializer'
end
it 'raises an exception when you declare both a static dependency and provide a custom initializer' do
err = assert_raises do
class FailingFoo5
include DependencySupport
dependency :qux, Qux, instance: Static.instance_bar, initializer: ->(foo) {}
end
end
assert_equal err.message, 'You cannot declare a dependency_initializer and provide an instance'
end
end
end
require 'mocha'
module DependencySupport
# A helper function to configure a mock dao to return the provided model
# and mount it to the provided system under test
#
# @param [ActiveRecord::Base] model
# @return [ActiveRecord::Base]
def mock_dao(model, save: false, save_exception: nil)
if save && save_exception.present?
fail 'You cannot configure a dao to both raise an error and save succesfully'
end
type = model.class.name.underscore
model.id = Faker::Number.rand_in_range(1, 1000)
mock_dao = Mocha::Mock.new(model.class.name)
mock_dao.stubs(:find).with(model.id).returns(model)
mock_dao.stubs(:all).returns([model])
mock_dao.stubs(:first).returns(model)
mock_dao.stubs(:last).returns(model)
# Mount the dependency on the system under test
self.substitute_dependency("#{type}_dao".to_sym, mock_dao)
if save
model.stubs(:save).returns(true)
model.stubs(:save!).returns(true)
end
if save_exception.present?
model.stubs(:save).raises(save_exception)
model.stubs(:save!).raises(save_exception)
end
model
end
# Builds Flipper in Test Mode (in memory mode) and mounts it to the system under test
#
# @return [Utils::FlipperUtil]
def setup_test_flipper
flipper = Utils::FlipperUtil.build_in_memory
self.substitute_dependency(:flipper, flipper)
flipper
end
# Builds a mock service and mounts it to the system under test
#
# @param [Symbol] service_name
# @param [Symbol[]] with
# @param [Mocha::Mock] returns
# @param [Symbol] method defaults to call
def mock_service(service_name, with: [], returns: nil, method: :call, times: 1)
mock_service = Mocha::Mock.new(service_name)
mock_service.expects(method).with(*with).returns(returns).times(times)
self.substitute_dependency(service_name, mock_service)
mock_service
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment