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