shoegaze 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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