bond-spy 0.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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE +27 -0
- data/README.rst +65 -0
- data/Rakefile +6 -0
- data/bin/bond_reconcile.py +385 -0
- data/bin/setup +5 -0
- data/bond.gemspec +36 -0
- data/lib/bond.rb +469 -0
- data/lib/bond/spec_helper.rb +32 -0
- data/lib/bond/targetable.rb +210 -0
- data/lib/bond/version.rb +3 -0
- data/spec/bond_spec.rb +158 -0
- data/spec/bond_targetable_spec.rb +202 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/test_observations/.gitignore +2 -0
- data/spec/test_observations/bond_spec/Bond_with_agents_should_call_doers_before_returning_result.json +14 -0
- data/spec/test_observations/bond_spec/Bond_with_agents_should_call_the_function_passed_as_result_if_it_is_callable.json +23 -0
- data/spec/test_observations/bond_spec/Bond_with_agents_should_correctly_call_a_single_doer_if_filter_criteria_are_met.json +10 -0
- data/spec/test_observations/bond_spec/Bond_with_agents_should_correctly_call_multiple_doers.json +13 -0
- data/spec/test_observations/bond_spec/Bond_with_agents_should_not_call_doers_of_overriden_agents.json +8 -0
- data/spec/test_observations/bond_spec/Bond_with_agents_should_override_old_agents_with_newer_agents.json +0 -0
- data/spec/test_observations/bond_spec/Bond_with_agents_should_throw_an_exception_if_specified_by_agent.json +9 -0
- data/spec/test_observations/bond_spec/Bond_with_agents_should_throw_the_result_of_the_value_passed_to_exception_if_callable.json +10 -0
- data/spec/test_observations/bond_spec/Bond_with_agents_should_work_with_multiple_agents_for_different_spy_points.json +23 -0
- data/spec/test_observations/bond_spec/Bond_with_agents_with_filters_should_override_old_agents_with_newer_agents_unless_theott8glo1xn.json +22 -0
- data/spec/test_observations/bond_spec/Bond_with_agents_with_filters_should_respect_combinations_of_filters.json +37 -0
- data/spec/test_observations/bond_spec/Bond_with_agents_with_filters_should_respect_function_filters.json +16 -0
- data/spec/test_observations/bond_spec/Bond_with_agents_with_filters_should_respect_single_key_value_filters_of_all_types.json +121 -0
- data/spec/test_observations/bond_spec/Bond_without_any_agents_should_correctly_log_nested_hashes_and_arrays_with_hash_sorting.json +31 -0
- data/spec/test_observations/bond_spec/Bond_without_any_agents_should_correctly_log_some_normal_arguments_with_a_spy_point_name.json +12 -0
- data/spec/test_observations/bond_spec/Bond_without_any_agents_should_correctly_log_some_normal_arguments_without_a_spy_point_name.json +10 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_continues_to_the_method_when_agent_result_continue_is_returned.json +10 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_continues_to_the_method_when_agent_result_none_is_returned.json +14 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_passes_through_blocks.json +10 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_returns_nil__and_mocks__when_an_agent_returns_nil.json +11 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_spies_private_methods.json +6 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_correctly_spies_protected_methods.json +6 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_class_method.json +7 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_method_with_variabl19nhijeqoo.json +13 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_mix_of_positional_a6qc3d4el92.json +33 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_mix_of_positional_aott8glo1xn.json +20 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_mix_of_required_andbcgjq06had.json +17 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_a_normal_method.json +7 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_all_optional_keyword_arguments.json +10 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_argument_types_correctly_spies_on_variable_keyword_arguments.json +22 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_changes_the_spy_point_naj4gnwvcu8n.json +5 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_errors_when_mocking_is_r9j7wklng0z.json +6 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_ignores_excluded_keys.json +10 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_mocks_when_one_is_specified.json +10 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_spies_the_return_value_w19nhijeqoo.json +10 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_different_spy_point_parameters_correctly_spies_the_return_value_ww8esw1qdxc.json +10 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_modules_correctly_spies_on_included_module_methods.json +6 -0
- data/spec/test_observations/bond_targetable_spec/BondTargetable_with_modules_correctly_spies_on_module_methods.json +7 -0
- data/tutorials/binary_search_tree/bst.rb +120 -0
- data/tutorials/binary_search_tree/bst_spec.rb +82 -0
- data/tutorials/binary_search_tree/run_tests.sh +4 -0
- data/tutorials/binary_search_tree/test_observations/.gitignore +2 -0
- data/tutorials/binary_search_tree/test_observations/bst_spec/Node_should_add_nodes_to_the_BST_correctly__testing_with_Bond.json +20 -0
- data/tutorials/binary_search_tree/test_observations/bst_spec/Node_should_add_nodes_to_the_BST_correctly__testing_without_Bond.json +3 -0
- data/tutorials/binary_search_tree/test_observations/bst_spec/Node_should_correctly_delete_nodes_from_the_BST.json +29 -0
- data/tutorials/heat_watcher/heat_watcher.rb +107 -0
- data/tutorials/heat_watcher/heat_watcher_spec.rb +116 -0
- data/tutorials/heat_watcher/run_tests.sh +4 -0
- data/tutorials/heat_watcher/test_observations/.gitignore +2 -0
- data/tutorials/heat_watcher/test_observations/heat_watcher_spec/HeatWatcher_should_properly_report_critical_errors.json +142 -0
- data/tutorials/heat_watcher/test_observations/heat_watcher_spec/HeatWatcher_should_properly_report_warnings_and_switch_back_to_OK_status.json +132 -0
- metadata +211 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative '../bond'
|
2
|
+
|
3
|
+
# This file defines a shared context which should be included
|
4
|
+
# into any RSpec test using Bond via:
|
5
|
+
#
|
6
|
+
# include_context :bond
|
7
|
+
#
|
8
|
+
# within your `describe` statement. It makes the `bond` variable
|
9
|
+
# available for you to use (for e.g. `bond.spy` and `bond.deploy_agent`)
|
10
|
+
# and automatically initializes Bond to be used in your tests.
|
11
|
+
#
|
12
|
+
# You may pass all of the same arguments to the `include_context` statement
|
13
|
+
# that you can to {Bond#start_test}. For example, to set the test name:
|
14
|
+
#
|
15
|
+
# include_context :bond, test_name: 'my_test_name'
|
16
|
+
#
|
17
|
+
|
18
|
+
shared_context :bond do |**settings|
|
19
|
+
|
20
|
+
let(:bond) { Bond.instance }
|
21
|
+
|
22
|
+
before :each do |example|
|
23
|
+
bond.start_test(example, **settings)
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
after :each do
|
28
|
+
if bond.send(:finish_test) == :bond_fail
|
29
|
+
fail('BOND_FAIL. Pass BOND_RECONCILE=[kdiff3|console|accept] environment variable to reconcile the observations.')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
# Module which should be `extend`ed into any class or module which you would like
|
2
|
+
# to spy on, either via {Bond#spy} or via a {#spy_point}. Within that class/module,
|
3
|
+
# the method {#bond} is available to access Bond, i.e. `bond.spy` and `bond.spy_point`.
|
4
|
+
#
|
5
|
+
# For example, if you would like to spy on MyClass, you would do this:
|
6
|
+
#
|
7
|
+
# class MyClass
|
8
|
+
# extend BondTargetable
|
9
|
+
#
|
10
|
+
# bond.spy_point(spy_point_name: 'my_name')
|
11
|
+
# def method_to_spy_on
|
12
|
+
# ...
|
13
|
+
# bond.spy(intermediate_state: state_variable)
|
14
|
+
# ...
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
module BondTargetable
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
# Keeps track of the arguments to the last spy_point annotation
|
23
|
+
@__last_annotation_args = nil
|
24
|
+
|
25
|
+
# The annotation which indicates that a method should be spied on. Use like:
|
26
|
+
#
|
27
|
+
# bond.spy_point(options...)
|
28
|
+
# def method_being_spied_on()
|
29
|
+
#
|
30
|
+
# This is safe to use in production code; it will have effects only if the function
|
31
|
+
# {Bond#start_test} has been called to initialize Bond.
|
32
|
+
# This will automatically spy every call to the `method_being_spied_on`. By default,
|
33
|
+
# all supplied arguments are logged. For normal arguments and keyword arguments,
|
34
|
+
# they will be logged as `argument_name: value`. For variable arguments,
|
35
|
+
# they are logged as `anonymous_parameter{arg_num}: value` (e.g. `anonymous_parameter0: 'foo'`).
|
36
|
+
# If the call to {Bond#spy} returns anything other than `:agent_result_none` or
|
37
|
+
# `:agent_result_continue` (i.e. through the use of {Bond#deploy_agent}), the spied
|
38
|
+
# method will never be called and the result will be returned in its place. Else, the
|
39
|
+
# method is called as normal.
|
40
|
+
#
|
41
|
+
# @param spy_point_name [String] If supplied, overrides the default spy point name, which is
|
42
|
+
# `Class.method_name` for class methods and `Class#method_name` for other methods.
|
43
|
+
# @param require_agent_result [Boolean] If true, you *must* supply a return value via
|
44
|
+
# {Bond#deploy_agent} during testing, else an error is thrown.
|
45
|
+
# @param excluded_keys [String, Symbol, Array<String, Symbol>] An array of key/argument
|
46
|
+
# names to exclude from the spy. Can be useful if you don't care about the value of some argument.
|
47
|
+
# @param spy_result [Boolean] If true, spy on the return value. The spy point name will be
|
48
|
+
# `{spy_point_name}.result` and the key name will be `result`.
|
49
|
+
# @api public
|
50
|
+
def spy_point(spy_point_name: nil, require_agent_result: false, excluded_keys: [],
|
51
|
+
spy_result: false)
|
52
|
+
@__last_annotation_args = {
|
53
|
+
spy_point_name: spy_point_name,
|
54
|
+
require_agent_result: require_agent_result,
|
55
|
+
# Allow for a single key or an array of them, and map to_s in case they were passed as symbols
|
56
|
+
excluded_keys: [*excluded_keys].map(&:to_s),
|
57
|
+
spy_result: spy_result
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
# Simple method to access Bond from within a BondTargetable class/module.
|
62
|
+
# @api public
|
63
|
+
def bond
|
64
|
+
PassthroughClass.new(self)
|
65
|
+
end
|
66
|
+
|
67
|
+
public
|
68
|
+
|
69
|
+
# Hook into method addition, if it was preceded by a call to #spy_point then spy on it.
|
70
|
+
# @param name Name of the method being added
|
71
|
+
# @private
|
72
|
+
def method_added(name)
|
73
|
+
super
|
74
|
+
return if @__last_annotation_args.nil?
|
75
|
+
annotation_args = @__last_annotation_args
|
76
|
+
@__last_annotation_args = nil
|
77
|
+
|
78
|
+
orig_method = instance_method(name)
|
79
|
+
|
80
|
+
if private_method_defined?(name)
|
81
|
+
visibility = :private
|
82
|
+
elsif protected_method_defined?(name)
|
83
|
+
visibility = :protected
|
84
|
+
else
|
85
|
+
visibility = :public
|
86
|
+
end
|
87
|
+
|
88
|
+
point_name = annotation_args.delete(:spy_point_name)
|
89
|
+
point_name = point_name_from_method(orig_method) if point_name.nil?
|
90
|
+
|
91
|
+
this = self
|
92
|
+
define_method(name) do |*args, **kwargs, &blk|
|
93
|
+
this.send(:bond_interceptor, orig_method.bind(self), point_name, annotation_args, *args, **kwargs, &blk)
|
94
|
+
end
|
95
|
+
|
96
|
+
case visibility
|
97
|
+
when :protected
|
98
|
+
protected name
|
99
|
+
when :private
|
100
|
+
private name
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Hook into singleton method addition, if it was preceded by a call to #spy_point then spy on it.
|
105
|
+
# @param name Name of the method being added
|
106
|
+
# @private
|
107
|
+
def singleton_method_added(name)
|
108
|
+
super
|
109
|
+
return if @__last_annotation_args.nil?
|
110
|
+
annotation_args = @__last_annotation_args
|
111
|
+
@__last_annotation_args = nil
|
112
|
+
|
113
|
+
orig_method = method(name)
|
114
|
+
|
115
|
+
point_name = annotation_args.delete(:spy_point_name)
|
116
|
+
point_name = point_name_from_method(orig_method) if point_name.nil?
|
117
|
+
|
118
|
+
this = self
|
119
|
+
this.define_singleton_method(name) do |*args, **kwargs, &blk|
|
120
|
+
this.send(:bond_interceptor, orig_method, point_name, annotation_args, *args, **kwargs, &blk)
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
# Method that gets wrapped around methods being spied on. Should never be called directly.
|
128
|
+
#
|
129
|
+
# @param method [Method] The method that is being spied on
|
130
|
+
# @param spy_point_name [String] The name of the spy point
|
131
|
+
# @param options [Hash] Hash of options, which should be any of the arguments to {#spy_point}
|
132
|
+
# except for `spy_point_name`
|
133
|
+
def bond_interceptor(method, spy_point_name, options, *args, **kwargs, &blk)
|
134
|
+
return kwargs.empty? ? method.call(*args, &blk) : method.call(*args, **kwargs, &blk) unless Bond.instance.active?
|
135
|
+
param_list = method.parameters.select { |type, _| type == :opt || type == :req }.map { |_, name| name }
|
136
|
+
|
137
|
+
observation = {}
|
138
|
+
anon_param_cnt = 0
|
139
|
+
args.zip(param_list) do |value, name|
|
140
|
+
if name.nil?
|
141
|
+
observation["anonymous_parameter#{anon_param_cnt}".to_sym] = value
|
142
|
+
else
|
143
|
+
observation[name] = value unless options[:excluded_keys].include?(name.to_s)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
kwargs.each do |name, value|
|
147
|
+
observation[name] = value unless options[:excluded_keys].include?(name.to_s)
|
148
|
+
end
|
149
|
+
|
150
|
+
ret = Bond.instance.spy(spy_point_name, observation)
|
151
|
+
if options[:require_agent_result] && ret == :agent_result_none
|
152
|
+
raise "#{spy_point_name} requires mocking but received :agent_result_none"
|
153
|
+
end
|
154
|
+
|
155
|
+
if ret == :agent_result_none || ret == :agent_result_continue
|
156
|
+
if kwargs.empty?
|
157
|
+
ret = method.call(*args, &blk)
|
158
|
+
else
|
159
|
+
ret = method.call(*args, **kwargs, &blk)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
Bond.instance.spy("#{spy_point_name}.result", result: ret) if options[:spy_result]
|
163
|
+
ret
|
164
|
+
end
|
165
|
+
|
166
|
+
# Extract the default point name from the method object, which is `Class.method_name`
|
167
|
+
# for class methods and `Class#method_name` for other methods.
|
168
|
+
def point_name_from_method(method)
|
169
|
+
method.inspect.to_s.sub(/#<[^:]+: ([^>]+)>/, '\1')
|
170
|
+
end
|
171
|
+
|
172
|
+
# A class that acts as if it was Bond by passing through all method calls
|
173
|
+
# *except* for `spy_point`, which it sends back to whatever object was passed
|
174
|
+
# in upon initialization (which should be something that `extend`s BondTargetable).
|
175
|
+
# This is used since calls to `spy_point` should be directed to BondTargetable
|
176
|
+
# and other calls should be directed to Bond, but we want this to happen
|
177
|
+
# transparently to the end-user.
|
178
|
+
# @private
|
179
|
+
class PassthroughClass
|
180
|
+
def initialize(parent)
|
181
|
+
@parent = parent
|
182
|
+
end
|
183
|
+
|
184
|
+
def spy_point(**kwargs)
|
185
|
+
@parent.send(:spy_point, **kwargs)
|
186
|
+
end
|
187
|
+
|
188
|
+
def method_missing(meth, *args)
|
189
|
+
Bond.instance.send(meth, *args)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# A module to export the `bond` method as an instance method in addition to
|
194
|
+
# a class method (which it will already appear as due to the `extend` statement)
|
195
|
+
# @private
|
196
|
+
module BondTargetableInstanceMethods
|
197
|
+
protected
|
198
|
+
def bond
|
199
|
+
PassthroughClass.new(self)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
public
|
204
|
+
# Used to mix in BondTargetableInstanceMethods, allowing for `bond` to appear as both
|
205
|
+
# an instance method and a class method.
|
206
|
+
# @private
|
207
|
+
def self.extended(base)
|
208
|
+
base.include(BondTargetableInstanceMethods)
|
209
|
+
end
|
210
|
+
end
|
data/lib/bond/version.rb
ADDED
data/spec/bond_spec.rb
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Bond do
|
4
|
+
include_context :bond
|
5
|
+
|
6
|
+
context 'without any agents' do
|
7
|
+
it 'should correctly log some normal arguments with a spy point name' do
|
8
|
+
bond.spy('my_point_name', spy_key: 1, spy_key_2: 'string value', spy_key_3: 1.4)
|
9
|
+
bond.spy('second_point', spy_key: 'string value 2')
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should correctly log some normal arguments without a spy point name' do
|
13
|
+
bond.spy(spy_key: 1, spy_key_2: 'string value', spy_key_3: 1.4)
|
14
|
+
bond.spy(spy_key: 'string value 2')
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should correctly log nested hashes and arrays with hash sorting' do
|
18
|
+
bond.spy(key1: {h1key1: %w(hello world), h1key2: 'hi'},
|
19
|
+
key2: [{key2: 'foo', key3: 'bar', key1: 'baz'}, 'array3', 'array2'])
|
20
|
+
bond.spy('point_name', key3: {key2: 'foo', key1: 'bar', key3: {hkey: 'hello world'}},
|
21
|
+
key1: 'value 1')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'with agents' do
|
26
|
+
it 'should work with multiple agents for different spy points' do
|
27
|
+
bond.deploy_agent('my_point_1', result: 'mock result')
|
28
|
+
bond.deploy_agent('my_point_2', result: 'mock result 2')
|
29
|
+
|
30
|
+
bond.spy('point_1_return', return_value: bond.spy('my_point_1'))
|
31
|
+
bond.spy('point_2_return', return_value: bond.spy('my_point_2'))
|
32
|
+
bond.spy('point_1_return_2', return_value: bond.spy('my_point_1'))
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'with filters' do
|
36
|
+
it 'should respect single key/value filters of all types' do
|
37
|
+
bond.deploy_agent('my_point', obs_name: 'value', result: 'mocked')
|
38
|
+
bond.spy(result: bond.spy('my_point', obs_name: 'value'))
|
39
|
+
bond.spy(result: bond.spy('my_point', obs_name: 'not value'))
|
40
|
+
|
41
|
+
bond.deploy_agent('my_point_eq', obs_name__eq: 'value', result: 'mocked')
|
42
|
+
bond.spy(result: bond.spy('my_point_eq', obs_name: 'value'))
|
43
|
+
bond.spy(result: bond.spy('my_point_eq', obs_name: 'not value'))
|
44
|
+
|
45
|
+
bond.deploy_agent('my_point_exact', obs_name__exact: 'value', result: 'mocked')
|
46
|
+
bond.spy(result: bond.spy('my_point_exact', obs_name: 'value'))
|
47
|
+
bond.spy(result: bond.spy('my_point_exact', obs_name: 'not value'))
|
48
|
+
|
49
|
+
bond.deploy_agent('my_point_startswith', obs_name__startswith: 'value', result: 'mocked')
|
50
|
+
bond.spy(result: bond.spy('my_point_startswith', obs_name: 'value'))
|
51
|
+
bond.spy(result: bond.spy('my_point_startswith', obs_name: 'not value'))
|
52
|
+
bond.spy(result: bond.spy('my_point_startswith', obs_name: 'value not'))
|
53
|
+
|
54
|
+
bond.deploy_agent('my_point_endswith', obs_name__endswith: 'value', result: 'mocked')
|
55
|
+
bond.spy(result: bond.spy('my_point_endswith', obs_name: 'value'))
|
56
|
+
bond.spy(result: bond.spy('my_point_endswith', obs_name: 'not value'))
|
57
|
+
bond.spy(result: bond.spy('my_point_endswith', obs_name: 'value not'))
|
58
|
+
|
59
|
+
bond.deploy_agent('my_point_contains', obs_name__contains: 'value', result: 'mocked')
|
60
|
+
bond.spy(result: bond.spy('my_point_contains', obs_name: 'value'))
|
61
|
+
bond.spy(result: bond.spy('my_point_contains', obs_name: 'not value'))
|
62
|
+
bond.spy(result: bond.spy('my_point_contains', obs_name: 'value not'))
|
63
|
+
bond.spy(result: bond.spy('my_point_contains', obs_name: 'not value not'))
|
64
|
+
bond.spy(result: bond.spy('my_point_contains', obs_name: 'foobar'))
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should respect function filters' do
|
68
|
+
bond.deploy_agent('my_point', filter: lambda { |obs| obs[:my_key].even? }, result: 'mocked')
|
69
|
+
bond.spy(result: bond.spy('my_point', my_key: 5))
|
70
|
+
bond.spy(result: bond.spy('my_point', my_key: 10))
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should override old agents with newer agents unless they are filtered out' do
|
74
|
+
bond.deploy_agent('my_point', result: 'first mock')
|
75
|
+
bond.deploy_agent('my_point', result: 'second mock')
|
76
|
+
bond.deploy_agent('my_point', obs_name: 'foobar', result: 'third mock')
|
77
|
+
|
78
|
+
bond.spy(result: bond.spy('my_point'))
|
79
|
+
bond.spy(result: bond.spy('my_point', obs_name: 'foobar'))
|
80
|
+
bond.spy(result: bond.spy('my_point', obs_name: 'baz'))
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should respect combinations of filters' do
|
84
|
+
bond.deploy_agent('my_point', result: 'mocked',
|
85
|
+
filter: lambda { |obs| obs.has_key?(:req_key) },
|
86
|
+
req_key2: 'value', req_key3__contains: 'value')
|
87
|
+
bond.spy(result: bond.spy('my_point', req_key: 'foo', req_key2: 'bar', req_key3: 'value'))
|
88
|
+
bond.spy(result: bond.spy('my_point', req_key: 'foo', req_key2: 'value', req_key3: 'foovaluebar'))
|
89
|
+
bond.spy(result: bond.spy('my_point', req_key: 'foo', req_key2: 'value', req_key3: 'lacking'))
|
90
|
+
bond.spy(result: bond.spy('my_point', req_key2: 'value', req_key3: 'value'))
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should call the function passed as result if it is callable' do
|
95
|
+
mocked_value = 1
|
96
|
+
bond.deploy_agent('my_point', result: lambda { |_| mocked_value += 1 }, obs_name: 'foo')
|
97
|
+
bond.spy(result: bond.spy('my_point', obs_name: 'foo'))
|
98
|
+
bond.spy(result: bond.spy('my_point', obs_name: 'bar'))
|
99
|
+
bond.spy(result: bond.spy('my_point', obs_name: 'foo'))
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'should throw an exception if specified by agent' do
|
103
|
+
bond.deploy_agent('my_point', exception: TypeError.new('TypeError exception!'))
|
104
|
+
begin
|
105
|
+
bond.spy('my_point')
|
106
|
+
rescue TypeError => e
|
107
|
+
bond.spy('rescue_point', except: e.to_s)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'should throw the result of the value passed to exception if callable' do
|
112
|
+
bond.deploy_agent('my_point', exception: lambda { |obs| ArgumentError.new(obs[:key]) })
|
113
|
+
begin
|
114
|
+
bond.spy('my_point', key: 'Value passed to exception')
|
115
|
+
rescue ArgumentError => e
|
116
|
+
bond.spy('rescue_point', except: e.to_s)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should correctly call a single doer if filter criteria are met' do
|
121
|
+
bond.deploy_agent('my_point', my_key__contains: '',
|
122
|
+
do: lambda { |obs| bond.spy('internal', val: obs[:my_key]) })
|
123
|
+
bond.spy('my_point', my_key: 'value')
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'should correctly call multiple doers' do
|
127
|
+
side_effect_value = 0
|
128
|
+
doers = [
|
129
|
+
lambda { |obs| bond.spy('internal', val: obs[:my_key]) },
|
130
|
+
lambda { |obs| side_effect_value += obs[:my_key] }
|
131
|
+
]
|
132
|
+
bond.deploy_agent('my_point', do: doers)
|
133
|
+
bond.spy('my_point', my_key: 10)
|
134
|
+
bond.spy(side_effect_value: side_effect_value)
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'should not call doers of overriden agents' do
|
138
|
+
bond.deploy_agent('my_point', do: lambda { |_| bond.spy('bad_agent') })
|
139
|
+
bond.deploy_agent('my_point', do: lambda { |_| bond.spy('valid_agent') })
|
140
|
+
bond.spy('my_point')
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'should call doers before returning result' do
|
144
|
+
bond.deploy_agent('my_point', do: lambda { |_| bond.spy('internal_doer') },
|
145
|
+
result: lambda { |_| bond.spy('internal_result'); 'mocked' })
|
146
|
+
bond.spy(result: bond.spy('my_point'))
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
# TODO
|
152
|
+
# overriding settings using Bond#settings
|
153
|
+
# some different start_test parameters
|
154
|
+
|
155
|
+
# finish test: creating output dir,
|
156
|
+
# some tests for SpyAgent and SpyAgentFilter - break out into separate files?
|
157
|
+
|
158
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe BondTargetable do
|
4
|
+
include_context :bond
|
5
|
+
|
6
|
+
class TestClass
|
7
|
+
extend BondTargetable
|
8
|
+
|
9
|
+
bond.spy_point
|
10
|
+
def annotated_standard_method(arg1, arg2) end
|
11
|
+
|
12
|
+
bond.spy_point
|
13
|
+
def self.annotated_class_method(arg1, arg2) end
|
14
|
+
|
15
|
+
bond.spy_point
|
16
|
+
def annotated_method_var_args(arg1, arg2, *args) end
|
17
|
+
|
18
|
+
bond.spy_point
|
19
|
+
def annotated_method_kw_params_mixed(arg1:, arg2: 'default', arg3: 'default') end
|
20
|
+
|
21
|
+
bond.spy_point
|
22
|
+
def annotated_method_kw_params_optional(arg1: 'default', arg2: 'default') end
|
23
|
+
|
24
|
+
bond.spy_point
|
25
|
+
def annotated_method_variable_kw_args(arg1:, arg2: 'default', **kwargs) end
|
26
|
+
|
27
|
+
bond.spy_point
|
28
|
+
def annotated_method_mixed_params(arg1, arg2, arg3: 'default', arg4: 'foobar') end
|
29
|
+
|
30
|
+
bond.spy_point
|
31
|
+
def annotated_method_mixed_variable_params(arg1, *args, arg_n1:, arg_n2: 'default', **kwargs) end
|
32
|
+
|
33
|
+
bond.spy_point(spy_point_name: 'my_name')
|
34
|
+
def annotated_method_with_name; end
|
35
|
+
|
36
|
+
bond.spy_point(excluded_keys: :arg1)
|
37
|
+
def annotated_method_single_exclude(arg1, arg2) end
|
38
|
+
|
39
|
+
bond.spy_point(excluded_keys: ['arg1', 'arg3'])
|
40
|
+
def annotated_method_multiple_exclude(arg1, arg2:, arg3: 'default') end
|
41
|
+
|
42
|
+
bond.spy_point(spy_point_name: 'mock_required', require_agent_result: true)
|
43
|
+
def annotated_method_mocking_required(arg1) 'return' end
|
44
|
+
|
45
|
+
bond.spy_point(spy_point_name: 'spy_return', spy_result: true)
|
46
|
+
def annotated_method_spy_return(arg1) 'return' end
|
47
|
+
|
48
|
+
bond.spy_point
|
49
|
+
def annotated_method_with_block(arg1, &blk) yield; end
|
50
|
+
|
51
|
+
def method_calling_protected; annotated_protected_method('value') end
|
52
|
+
|
53
|
+
def method_calling_private; annotated_private_method('value') end
|
54
|
+
|
55
|
+
protected
|
56
|
+
spy_point
|
57
|
+
def annotated_protected_method(arg1) end
|
58
|
+
|
59
|
+
private
|
60
|
+
spy_point
|
61
|
+
def annotated_private_method(arg1) end
|
62
|
+
end
|
63
|
+
|
64
|
+
let (:tc) { TestClass.new }
|
65
|
+
|
66
|
+
context 'with different argument types' do
|
67
|
+
it 'correctly spies on a normal method' do
|
68
|
+
tc.annotated_standard_method(1, 2)
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'correctly spies on a class method' do
|
72
|
+
TestClass.annotated_class_method(1, 2)
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'correctly spies on a method with variable arguments' do
|
76
|
+
tc.annotated_method_var_args('value1', 'value2')
|
77
|
+
tc.annotated_method_var_args('value1', 'value2', 'value3', 'value4')
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'correctly spies on a mix of required and optional keyword arguments' do
|
81
|
+
tc.annotated_method_kw_params_mixed(arg1: 'value')
|
82
|
+
tc.annotated_method_kw_params_mixed(arg1: 'value', arg3: 'new value')
|
83
|
+
tc.annotated_method_kw_params_mixed(arg1: 'value', arg3: 'new value3', arg2: 'new value2')
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'correctly spies on all optional keyword arguments' do
|
87
|
+
tc.annotated_method_kw_params_optional
|
88
|
+
tc.annotated_method_kw_params_optional(arg1: 'value1', arg2: 'value2')
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'correctly spies on variable keyword arguments' do
|
92
|
+
tc.annotated_method_variable_kw_args(arg1: 'value1')
|
93
|
+
tc.annotated_method_variable_kw_args(arg1: 'value1', arg2: 'value2')
|
94
|
+
tc.annotated_method_variable_kw_args(arg1: 'value1', arg3: 'value3')
|
95
|
+
tc.annotated_method_variable_kw_args(arg1: 'value1', arg4: 'value4', arg3: 'value3')
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'correctly spies on a mix of positional and named arguments' do
|
99
|
+
tc.annotated_method_mixed_params('foo', 'bar')
|
100
|
+
tc.annotated_method_mixed_params('foo', 'bar', arg4: 'new value4')
|
101
|
+
tc.annotated_method_mixed_params('foo', 'bar', arg3: 'new value3', arg4: 'new value4')
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'correctly spies on a mix of positional and named arguments, both variable' do
|
105
|
+
tc.annotated_method_mixed_variable_params('value1', arg_n1: 'value_n1')
|
106
|
+
tc.annotated_method_mixed_variable_params('value1', arg_n1: 'value_n1', arg_n2: 'value_n2')
|
107
|
+
tc.annotated_method_mixed_variable_params('value1', 'value2', 'value3', arg_n1: 'value_n1', arg_n2: 'value_n2')
|
108
|
+
tc.annotated_method_mixed_variable_params('value1', 'value2', 'value3', arg_n1: 'value_n1')
|
109
|
+
tc.annotated_method_mixed_variable_params('value1', 'value2', 'value3', arg_n1: 'value_n1', arg_n3: 'value_n3')
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'with different spy_point parameters' do
|
114
|
+
it 'correctly changes the spy point name if it is specified' do
|
115
|
+
tc.annotated_method_with_name
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'correctly ignores excluded keys' do
|
119
|
+
tc.annotated_method_single_exclude('value1', 'value2')
|
120
|
+
tc.annotated_method_multiple_exclude('value1', arg2: 'value2', arg3: 'value3')
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'correctly errors when mocking is required and none is specified' do
|
124
|
+
begin
|
125
|
+
tc.annotated_method_mocking_required(2)
|
126
|
+
fail
|
127
|
+
rescue RuntimeError
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'correctly mocks when one is specified' do
|
132
|
+
bond.deploy_agent('mock_required', result: 'new return')
|
133
|
+
ret = tc.annotated_method_mocking_required(5)
|
134
|
+
bond.spy('return_value', return: ret)
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'correctly spies the return value when not mocking' do
|
138
|
+
tc.annotated_method_spy_return('arg_value')
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'correctly spies the return value when mocking' do
|
142
|
+
bond.deploy_agent('spy_return', result: 'new return')
|
143
|
+
tc.annotated_method_spy_return('arg_value')
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'correctly spies protected methods' do
|
148
|
+
tc.method_calling_protected
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'correctly spies private methods' do
|
152
|
+
tc.method_calling_private
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'correctly continues to the method when agent_result_continue is returned' do
|
156
|
+
bond.deploy_agent('mock_required', result: :agent_result_continue)
|
157
|
+
ret = tc.annotated_method_mocking_required('value1')
|
158
|
+
bond.spy('return value', ret: ret)
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'correctly continues to the method when agent_result_none is returned' do
|
162
|
+
bond.deploy_agent('spy_return', result: :agent_result_none)
|
163
|
+
ret = tc.annotated_method_spy_return('value')
|
164
|
+
bond.spy('return value', ret: ret)
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'correctly passes through blocks' do
|
168
|
+
ret = tc.annotated_method_with_block('foo') { 'value' }
|
169
|
+
bond.spy('return value', ret: ret)
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'correctly returns nil (and mocks) when an agent returns nil' do
|
173
|
+
arr = [0]
|
174
|
+
bond.deploy_agent('TestClass#annotated_method_with_block', result: nil)
|
175
|
+
ret = tc.annotated_method_with_block('foo') { arr[0] = 1 }
|
176
|
+
bond.spy('return value', ret: ret, arr_val: arr[0])
|
177
|
+
end
|
178
|
+
|
179
|
+
context 'with modules' do
|
180
|
+
module TestModule
|
181
|
+
extend BondTargetable
|
182
|
+
|
183
|
+
bond.spy_point
|
184
|
+
def self.annotated_class_method(arg1, arg2) end
|
185
|
+
|
186
|
+
bond.spy_point
|
187
|
+
def annotated_standard_method(arg1) end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
class TestClassWithMixin; include TestModule; end
|
192
|
+
|
193
|
+
it 'correctly spies on module methods' do
|
194
|
+
TestModule.annotated_class_method('value1', 'value2')
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'correctly spies on included module methods' do
|
198
|
+
TestClassWithMixin.new.annotated_standard_method('value')
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
end
|