shoegaze 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +190 -0
  3. data/lib/shoegaze.rb +15 -0
  4. data/lib/shoegaze/datastore.rb +36 -0
  5. data/lib/shoegaze/implementation.rb +71 -0
  6. data/lib/shoegaze/mock.rb +34 -0
  7. data/lib/shoegaze/model.rb +165 -0
  8. data/lib/shoegaze/proxy.rb +24 -0
  9. data/lib/shoegaze/proxy/interface.rb +169 -0
  10. data/lib/shoegaze/proxy/template.rb +61 -0
  11. data/lib/shoegaze/scenario.rb +100 -0
  12. data/lib/shoegaze/scenario/mock.rb +52 -0
  13. data/lib/shoegaze/scenario/orchestrator.rb +167 -0
  14. data/lib/shoegaze/version.rb +3 -0
  15. data/spec/features/basic_class_method_spec.rb +55 -0
  16. data/spec/features/basic_instance_method_spec.rb +53 -0
  17. data/spec/features/class_default_scenario_spec.rb +51 -0
  18. data/spec/features/class_method_block_argument_spec.rb +61 -0
  19. data/spec/features/data_store_spec.rb +73 -0
  20. data/spec/features/instance_customized_initializer.rb +24 -0
  21. data/spec/features/instance_default_scenario_spec.rb +51 -0
  22. data/spec/features/instance_method_block_argument_spec.rb +60 -0
  23. data/spec/features/nested_class_method_spec.rb +47 -0
  24. data/spec/features/nested_instance_method_spec.rb +47 -0
  25. data/spec/shoegaze/datastore_spec.rb +38 -0
  26. data/spec/shoegaze/implementation_spec.rb +97 -0
  27. data/spec/shoegaze/mock_spec.rb +42 -0
  28. data/spec/shoegaze/proxy/interface_spec.rb +88 -0
  29. data/spec/shoegaze/proxy/template_spec.rb +36 -0
  30. data/spec/shoegaze/proxy_spec.rb +18 -0
  31. data/spec/shoegaze/scenario/mock_spec.rb +31 -0
  32. data/spec/shoegaze/scenario/orchestrator_spec.rb +145 -0
  33. data/spec/shoegaze/scenario_spec.rb +74 -0
  34. metadata +133 -0
@@ -0,0 +1,24 @@
1
+ require_relative 'proxy/template'
2
+ require_relative 'proxy/interface'
3
+
4
+ module Shoegaze
5
+ module Proxy
6
+ # Creates a Shoegaze mock proxy that delegates all the class method calls to the class
7
+ # double and all the instance method calls to the instance double.
8
+ #
9
+ # @param mock_class_double [RSpec::Mocks::ClassVerifyingDouble] RSpec class double that will receive class method calls
10
+ # @param mock_instance_double [RSpec::Mocks::InstanceVerifyingDouble] RSpec instance double that will receive instance method calls
11
+ # @return [Class.new(Shoegaze::Proxy)] The created Shoegaze proxy.
12
+ #
13
+ # The goal here is to create a bare-bones anonymous class that delegates all class
14
+ # methods to the class double and all instance methods to the instance double such that
15
+ # it implements almost nothing else to avoid conflicts with the actual implementations.
16
+ def self.new(mock_class_double, mock_instance_double)
17
+ proxy = Class.new(Template)
18
+ proxy.class_double = mock_class_double
19
+ proxy.instance_double = mock_instance_double
20
+
21
+ proxy
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,169 @@
1
+ module Shoegaze
2
+ module Proxy
3
+ # A common interface module for defining mock implementations, scenarios, and driving
4
+ # the implementations/scenarios from the test interface.
5
+ module Interface
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ base.class_eval do
9
+ extend RSpec::Mocks::ExampleMethods
10
+
11
+ class << self
12
+ attr_reader :implementations
13
+ end
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+ # Defines a named Shoegaze implementation for a class method.
19
+ #
20
+ # @param method_name [Symbol] Symbol name for the class method that is being implemented
21
+ # @param block [Block] Shoegaze::Implementation expressed in a block
22
+ # @return [Shoegaze::Implementation] The created implementation.
23
+ #
24
+ # example:
25
+ #
26
+ # class FakeThing < Shoegaze::Mock
27
+ # mock "RealThing"
28
+ #
29
+ # implement_class_method :find_significant_other do
30
+ # default do
31
+ # datasource do
32
+ # :ohhai
33
+ # end
34
+ # end
35
+ # end
36
+ # end
37
+ #
38
+ # example usage:
39
+ #
40
+ # $ FakeThing.proxy.find_significant_other
41
+ # :ohhai
42
+ def implement_class_method(method_name, &block)
43
+ implementations[:class][method_name] = Implementation.new(self, @mock_class_double, :class, method_name, &block)
44
+ end
45
+
46
+ # Defines a named Shoegaze implementation for a instance method.
47
+ #
48
+ # @param method_name [Symbol] Symbol name for the instance method that is being implemented
49
+ # @param block [Block] Shoegaze::Implementation expressed in a block
50
+ # @return [Shoegaze::Implementation] The created implementation.
51
+ #
52
+ # example:
53
+ #
54
+ # class FakeThing < Shoegaze::Mock
55
+ # mock "RealThing"
56
+ #
57
+ # implement_instance_method :find_significant_other do
58
+ # default do
59
+ # datasource do
60
+ # :ohhai
61
+ # end
62
+ # end
63
+ # end
64
+ # end
65
+ #
66
+ # example usage:
67
+ #
68
+ # $ FakeThing.proxy.new.find_significant_other
69
+ # :ohhai
70
+ def implement_instance_method(method_name, &block)
71
+ implementations[:instance][method_name] = Implementation.new(self, @mock_instance_double, :instance, method_name, &block)
72
+ end
73
+
74
+ alias_method :implement, :implement_instance_method
75
+
76
+ # Defines a Scenario::Orchestrator for the method name, which is used to trigger
77
+ # particular scenarios when the class method is called on the mock proxy.
78
+ #
79
+ # @param method_name [Symbol] Symbol name for the class method that is being orchestrated.
80
+ # @return [Shoegaze::Orchestrator] The created Shoegaze orchestration.
81
+ #
82
+ # example:
83
+ #
84
+ # class FakeThing < Shoegaze::Mock
85
+ # mock "RealThing"
86
+ #
87
+ # implement_instance_method :find_significant_other do
88
+ # scenario :success do
89
+ # datasource do
90
+ # :ohhai
91
+ # end
92
+ # end
93
+ # end
94
+ # end
95
+ #
96
+ # example usage:
97
+ #
98
+ # $ FakeThing.proxy.class_call(:find_significant_other).with(:wow).yields(:success)
99
+ # $ FakeThing.proxy.find_significant_other(:wow)
100
+ # :ohhai
101
+ #
102
+ def class_call(method_name)
103
+ Scenario::Orchestrator.new(self, @mock_class_double, :class, method_name)
104
+ end
105
+
106
+ # Defines a Scenario::Orchestrator for the method name, which is used to trigger
107
+ # particular scenarios when the instance method is called on the mock proxy.
108
+ #
109
+ # @param method_name [Symbol] Symbol name for the instance method that is being orchestrated.
110
+ # @return [Shoegaze::Orchestrator] The created Shoegaze orchestration.
111
+ #
112
+ # example:
113
+ #
114
+ # class FakeThing < Shoegaze::Mock
115
+ # mock "RealThing"
116
+ #
117
+ # implement_instance_method :find_significant_other do
118
+ # scenario :success do
119
+ # datasource do
120
+ # :ohhai
121
+ # end
122
+ # end
123
+ # end
124
+ # end
125
+ #
126
+ # example usage:
127
+ #
128
+ # $ FakeThing.proxy.instance_call(:find_significant_other).with(:wow).yields(:success)
129
+ # $ FakeThing.proxy.new.find_significant_other(:wow)
130
+ # :ohhai
131
+ #
132
+ def instance_call(method_name)
133
+ Scenario::Orchestrator.new(self, @mock_instance_double, :instance, method_name)
134
+ end
135
+
136
+ alias_method :calling, :instance_call
137
+
138
+ # Creates an anonymous class inherited from Shoegaze::Proxy that delegates method
139
+ # calls to the proxy instance and class doubles. This is the stand-in for your
140
+ # real implementation.
141
+ #
142
+ # @return [Class.new(Shoegaze::Proxy)] A Shoegaze proxy class stand-in for the real implementation.
143
+ def proxy
144
+ @proxy ||= Shoegaze::Proxy.new(@mock_class_double, @mock_instance_double)
145
+ end
146
+
147
+ private
148
+
149
+ # Rspec doubles don't let us use them outside of tests, which is pretty annoying
150
+ # because the 'default' scenario method needs to set up a scenario outside of the
151
+ # testing scope. combine that with how rspec also overrides respond_to? and you
152
+ # end up with a lovely hack like this
153
+ def extend_double_with_extra_methods(double)
154
+ double.instance_eval do
155
+ @default_scenarios = {}
156
+
157
+ def add_default_scenario(method_name, implementation)
158
+ @default_scenarios[method_name] = implementation
159
+ end
160
+
161
+ def default_scenario(method_name)
162
+ @default_scenarios[method_name]
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,61 @@
1
+ module Shoegaze
2
+ module Proxy
3
+ # Provides the basic 'template' for our anonymous proxy classes whose *only purpose* is to
4
+ # delegate implementation method calls to the class and instance doubles.
5
+ class Template
6
+ class << self
7
+ attr_accessor :class_double
8
+ attr_accessor :instance_double
9
+
10
+ def method_missing(method, *args, &block)
11
+ # yeah, we are abusing re-use of rspec doubles
12
+ class_double.instance_variable_set(:@__expired, false)
13
+
14
+ default_scenario = class_double.default_scenario(method)
15
+
16
+ if class_double.respond_to?(method)
17
+ return class_double.send(method, *args, &block)
18
+ end
19
+
20
+ return default_scenario.call(*args, &block) if default_scenario
21
+
22
+ begin
23
+ super
24
+ rescue NoMethodError
25
+ raise_no_implementation_error(method, class_double)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def raise_no_implementation_error(method, double)
32
+ raise Shoegaze::Scenario::Orchestrator::NoImplementationError.new("#{self.name} either has no Shoegaze mock implementation or no scenario has been orchestrated for method :#{method}")
33
+ end
34
+ end
35
+
36
+ def initialize(*args)
37
+ # no-op to allow newifying instances
38
+ # NOTE: due to complexity, mocking of :initialize is not actually supported. drive
39
+ # your behaviors via other methods
40
+ end
41
+
42
+ def method_missing(method, *args, &block)
43
+ double = self.class.instance_double
44
+
45
+ # yeah, we are abusing re-use of rspec doubles
46
+ double.instance_variable_set(:@__expired, false)
47
+
48
+ default_scenario = double.default_scenario(method)
49
+
50
+ return double.send(method, *args, &block) if double.respond_to?(method)
51
+ return default_scenario.call(*args, &block) if default_scenario
52
+
53
+ begin
54
+ super
55
+ rescue NoMethodError
56
+ self.class.raise_no_implementation_error(method, double)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,100 @@
1
+ require_relative 'scenario/mock'
2
+ require_relative 'scenario/orchestrator'
3
+
4
+ module Shoegaze
5
+ class Scenario
6
+ def initialize(method_name, &block)
7
+ @_method_name = method_name
8
+
9
+ self.instance_eval(&block)
10
+ end
11
+
12
+ # Specifies the (optional) representer to use when returning the evaluated data
13
+ # source. If no representer is specified, the data source itself is returned
14
+ # untouched. You can specify either a stand-alone Representable::Decorator, or provide
15
+ # an inline one implementation via a block. This is a getter and a setter. Call it with no
16
+ # arg to _get_ the current representer.
17
+ #
18
+ # @param representer_class [Representable::Decorator] (optional) A decorator class that will be used to wrap the result of the data source.
19
+ # @param representer_block [Block] (optional) An inline Representable::Decorator implementation expressed as a block.
20
+ # @return [Representable::Decorator] The created or referenced representer.
21
+ #
22
+ # example:
23
+ #
24
+ # class FakeThing < Shoegaze::Mock
25
+ # mock "RealThing"
26
+ #
27
+ # implement :method do
28
+ # scenario :success do
29
+ # representer ThingRepresenter
30
+ #
31
+ # # or...
32
+ #
33
+ # representer do
34
+ # property :id
35
+ # property :name
36
+ # end
37
+ # end
38
+ # end
39
+ # end
40
+ #
41
+ def representer(representer_class = nil, &block)
42
+ if representer_class
43
+ @_representer = representer_class
44
+ end
45
+
46
+ if block_given?
47
+ @_representer = Class.new(Representable::Decorator).class_eval do
48
+ self.class_eval(&block)
49
+ self
50
+ end
51
+ end
52
+
53
+ @_representer
54
+ end
55
+
56
+ # Specifies the method to call on the scenario's representer. Common examples as
57
+ # :as_json or :to_json. You can omit this and, if a representer is specified, the
58
+ # representer itself will be returned. This is a getter and a setter. Call it with no
59
+ # arg to _get_ the current representation_method.
60
+ #
61
+ # @param representation_method [Symbol] The method to call on the Representable::Decorator, the result of which is ultimately returned out of the implementation of this scenario.
62
+ # @return [Symbol] The representation method
63
+ #
64
+ # example:
65
+ #
66
+ # class FakeThing < Shoegaze::Mock
67
+ # mock "RealThing"
68
+ #
69
+ # implement :method do
70
+ # scenario :success do
71
+ # representer ThingRepresenter
72
+ # represent_method :as_json
73
+ # end
74
+ # end
75
+ # end
76
+ #
77
+ def represent_method(representation_method = nil)
78
+ if representation_method
79
+ @_representation_method = representation_method
80
+ end
81
+
82
+ @_representation_method
83
+ end
84
+
85
+ # Specifies the datasource (actual implementation code) for the implementation scenario. This is ruby code in a block. The result of this block is fed into the scenario representer, if one is specified, or returned untouched if the scenario is not represented.
86
+ #
87
+ # @param block [Block] The implementation for the scenario's data source expressed as a block.
88
+ # @return [Block] The block
89
+ def datasource(&block)
90
+ @_datasource = block
91
+ end
92
+
93
+ # This just returns the datasource block.
94
+ #
95
+ # @return [Block] The current data source block
96
+ def to_proc
97
+ @_datasource
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,52 @@
1
+ module Shoegaze
2
+ class Scenario
3
+ class Mock
4
+ include Proxy::Interface
5
+
6
+ class << self
7
+ # This is a mocking interface for all mocking contexts except for the top-level
8
+ # mocking context. in other words, when you're chaining implementation calls, this
9
+ # interface will be used beyond the top-level context. It behaves almost exactly
10
+ # like Shoegaze::Mock.
11
+ #
12
+ # example:
13
+ #
14
+ # class Fake < Shoegaze::Mock
15
+ # mock "Real"
16
+ #
17
+ # implement :accounts do # top-level Shoegaze::Mock interface
18
+ # scenario :success do
19
+ # datasource do
20
+ # implement :create do # Shoegaze::ScenarioMock interface from now on
21
+ # default do
22
+ # datasource do
23
+ # implement :even_more_things # yup, still Shoegaze::ScenarioMock...
24
+ # default do
25
+ # datasource do |params|
26
+ # OkayFinally.new(params)
27
+ # end
28
+ # end
29
+ # end
30
+ # end
31
+ # end
32
+ # end
33
+ # end
34
+ # end
35
+ # end
36
+ # end
37
+ def mock(_nothing = nil)
38
+ @_mock_class = double
39
+ @mock_class_double = double
40
+ @mock_instance_double = double
41
+
42
+ extend_double_with_extra_methods(@mock_instance_double)
43
+ extend_double_with_extra_methods(@mock_class_double)
44
+
45
+ @implementations = {class: {}, instance: {}}
46
+
47
+ proxy
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,167 @@
1
+ module Shoegaze
2
+ class Scenario
3
+ class Orchestrator
4
+ include RSpec::Mocks::ExampleMethods
5
+
6
+ class NoImplementationError < StandardError; end
7
+
8
+ def initialize(mock_class, mock_double, scope, method_name)
9
+ @_scope = scope
10
+ @_mock_class = mock_class
11
+ @_mock_double = mock_double
12
+ @_method_name = method_name
13
+ end
14
+
15
+ # Specifies the arguments to which this scenario will be scoped.
16
+ #
17
+ # @param args [*Arguments] any number of free-form arguments
18
+ # @return [Shoegaze::Scenario::Orchestrator] returns the orchestrator `self` for chainability
19
+ #
20
+ # example:
21
+ #
22
+ # FakeThing.proxy.calling(:find_cows).with(123, 456).yields(:success)
23
+ #
24
+ def with(*args)
25
+ @_args = args
26
+ self
27
+ end
28
+
29
+ # Specifies the scenario for the implementation that will be triggered when this method is
30
+ # called with the specified scope (arguments, etc).
31
+ #
32
+ # @param scenario_name [Symbol] The name of the scenario to trigger.
33
+ # @return [Shoegaze::Scenario::Orchestrator] returns the orchestrator `self` for chainability
34
+ #
35
+ # example:
36
+ #
37
+ # FakeThing.proxy.calling(:find_cows).with(123, 456).yields(:success)
38
+ #
39
+ def yields(scenario_name)
40
+ implementation = @_mock_class.implementations[@_scope][@_method_name]
41
+
42
+ scenario = begin
43
+ implementation.scenarios[scenario_name]
44
+ rescue NoMethodError
45
+ end
46
+
47
+ unless scenario
48
+ raise NoImplementationError.new(
49
+ "#{@_mock_class} has no implementation for scenario :#{scenario_name} of the #{@_scope} method :#{@_method_name}."
50
+ )
51
+ end
52
+
53
+ # yeah, we are abusing re-use of rspec doubles
54
+ @_mock_double.instance_variable_set(:@__expired, false)
55
+
56
+ args = @_args
57
+
58
+ if @_args.nil?
59
+ args = [anything]
60
+
61
+ # also allow no args if no args are specified
62
+ send(:allow, @_mock_double).to receive(@_method_name) do
63
+ execute_scenario(scenario)
64
+ end
65
+ end
66
+
67
+ send(
68
+ :allow,
69
+ @_mock_double
70
+ ).to receive(@_method_name).with(*args) do |*_args, &datasource_block|
71
+ execute_scenario(scenario, &datasource_block)
72
+ end
73
+
74
+ self
75
+ end
76
+
77
+ # Executes the specified implementation scenario.
78
+ #
79
+ # @param scenario_name [Symbol] The name of the scenario to run.
80
+ # @yield [datasource_result] yields the result of the provided datasource block
81
+ # @return [Misc] returns the represented result of the scenario
82
+ #
83
+ def execute_scenario(scenario, &datasource_block)
84
+ # we do this crazy dance because we want scenario.to_proc to be run in the context
85
+ # of self (an orchestrator) in order to enable nesting, but we also want to be
86
+ # able to pass in a block. instance_exec would solve the context problem but
87
+ # doesn't enable the passing of the block while simply calling the method would
88
+ # allow passing the block but not changing the context.
89
+ self.define_singleton_method :bound_proc, &scenario.to_proc
90
+ data = self.bound_proc(*@_args, &datasource_block)
91
+
92
+ represent(data, scenario)
93
+ end
94
+
95
+ # Specifies a sub-implementation proxy interface used for recursive chaining of
96
+ # implementations. Think of it as a recursable Shoegaze::Mock.implement_class_method.
97
+ # All sub-implementations are internally implemented as class methods for simplicity.
98
+ #
99
+ # @param method_name [Symbol] The name of the nested method to implement
100
+ # @return [Class.new(Shoegaze::Proxy)] A Shoegaze proxy for next layer of the implementation.
101
+ #
102
+ # example:
103
+ #
104
+ # class Fake < Shoegaze::Mock
105
+ # mock "Real"
106
+ #
107
+ # implement :accounts do # top-level Shoegaze::Mock interface
108
+ # scenario :success do
109
+ # datasource do
110
+ # implement :create do # _this method_
111
+ # default do
112
+ # datasource do
113
+ # implement :even_more_things # _this method again_
114
+ # default do
115
+ # datasource do |params|
116
+ # :popcorn
117
+ # end
118
+ # end
119
+ # end
120
+ # end
121
+ # end
122
+ # end
123
+ # end
124
+ # end
125
+ # end
126
+ # end
127
+ #
128
+ # $ Fake.accounts.create.even_more_things
129
+ # :popcorn
130
+ #
131
+ def implement(method_name, &block)
132
+ proxy_interface.implement_class_method(method_name, &block)
133
+ proxy_interface.proxy
134
+ end
135
+
136
+ private
137
+
138
+ def allowance
139
+ case @_scope
140
+ when :instance
141
+ :allow_any_instance_of
142
+ when :class
143
+ :allow
144
+ end
145
+ end
146
+
147
+ def represent(data, scenario)
148
+ return data unless scenario.representer
149
+
150
+ representer = scenario.representer.new(data)
151
+ return representer unless scenario.represent_method
152
+
153
+ representer.send(scenario.represent_method)
154
+ end
155
+
156
+ # creates a new mocking context for the nested method call
157
+ # see Shoegaze::Scenario::Mock
158
+ def proxy_interface
159
+ return @_proxy_interface if @_proxy_interface
160
+
161
+ @_proxy_interface = Class.new(Scenario::Mock)
162
+ @_proxy_interface.mock
163
+ @_proxy_interface
164
+ end
165
+ end
166
+ end
167
+ end