impersonator 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +71 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +138 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +59 -0
- data/LICENSE.txt +21 -0
- data/README.md +200 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/impersonator.gemspec +40 -0
- data/lib/impersonator.rb +20 -0
- data/lib/impersonator/api.rb +68 -0
- data/lib/impersonator/block_invocation.rb +3 -0
- data/lib/impersonator/block_spy.rb +20 -0
- data/lib/impersonator/configuration.rb +17 -0
- data/lib/impersonator/double.rb +15 -0
- data/lib/impersonator/errors/configuration_error.rb +6 -0
- data/lib/impersonator/errors/method_invocation_error.rb +6 -0
- data/lib/impersonator/has_logger.rb +7 -0
- data/lib/impersonator/method.rb +44 -0
- data/lib/impersonator/method_invocation.rb +3 -0
- data/lib/impersonator/method_matching_configuration.rb +13 -0
- data/lib/impersonator/proxy.rb +56 -0
- data/lib/impersonator/recording.rb +120 -0
- data/lib/impersonator/version.rb +3 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c27e2c12811df79ee6ecda2a617b9952ac98dff360ca3e08d725832d54cf5b03
|
4
|
+
data.tar.gz: d40825fb2fbbdc7ac842bf58864acc2c02f2dd72b3a228b1ee0ee9ded86f891b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e8039d138b2f899a27d9476873ce687586a5322a7d0846a2828207592f51fd6a80f134a54fc7dce99cc8ea1d8581cdae25f62382e2129054d2148d1d05360543
|
7
|
+
data.tar.gz: e39e557b268d09c24206d7db469fa7dd931002e0cf9b6b72b70e25ba2f87270f43b176170b43e83ae7b8fa0e26999c7e956afb1b5ac83a1fd2f0327359124e94
|
@@ -0,0 +1,71 @@
|
|
1
|
+
version: 2.1
|
2
|
+
defaults: &defaults
|
3
|
+
working_directory: ~/impersonator
|
4
|
+
docker:
|
5
|
+
- image: circleci/ruby:2.6.2-node-browsers
|
6
|
+
environment:
|
7
|
+
BUNDLE_PATH: vendor/bundle
|
8
|
+
PGHOST: 127.0.0.1
|
9
|
+
PGUSER: impersonator
|
10
|
+
commands:
|
11
|
+
prepare:
|
12
|
+
description: "Common preparation steps"
|
13
|
+
steps:
|
14
|
+
- checkout
|
15
|
+
|
16
|
+
- restore_cache:
|
17
|
+
keys:
|
18
|
+
- v1-dependencies-{{ checksum "Gemfile.lock" }}
|
19
|
+
# fallback to using the latest cache if no exact match is found
|
20
|
+
- v1-dependencies-
|
21
|
+
- run:
|
22
|
+
name: install dependencies
|
23
|
+
command: |
|
24
|
+
bundle install --jobs=4 --retry=3 --path vendor/bundle
|
25
|
+
|
26
|
+
- save_cache:
|
27
|
+
paths:
|
28
|
+
- ./vendor/bundle
|
29
|
+
key: v1-dependencies-{{ checksum "Gemfile.lock" }}
|
30
|
+
|
31
|
+
jobs:
|
32
|
+
tests:
|
33
|
+
<<: *defaults
|
34
|
+
steps:
|
35
|
+
- prepare
|
36
|
+
- run:
|
37
|
+
name: run tests
|
38
|
+
command: |
|
39
|
+
mkdir /tmp/test-results
|
40
|
+
TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)"
|
41
|
+
|
42
|
+
bundle exec rspec --format progress \
|
43
|
+
--format RspecJunitFormatter \
|
44
|
+
--out /tmp/test-results/rspec.xml \
|
45
|
+
--tag ~type:performance \
|
46
|
+
--format progress \
|
47
|
+
$TEST_FILES
|
48
|
+
|
49
|
+
# collect reports
|
50
|
+
- store_test_results:
|
51
|
+
path: /tmp/test-results
|
52
|
+
- store_artifacts:
|
53
|
+
path: /tmp/test-results
|
54
|
+
destination: test-results
|
55
|
+
rubocop:
|
56
|
+
<<: *defaults
|
57
|
+
steps:
|
58
|
+
- prepare
|
59
|
+
- run:
|
60
|
+
name: Rubocop
|
61
|
+
command: bundle exec rubocop
|
62
|
+
workflows:
|
63
|
+
version: 2
|
64
|
+
pipeline:
|
65
|
+
jobs:
|
66
|
+
- tests
|
67
|
+
- rubocop
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
require: rubocop-rspec
|
2
|
+
|
3
|
+
Style/FrozenStringLiteralComment:
|
4
|
+
Enabled: false
|
5
|
+
|
6
|
+
Metrics/LineLength:
|
7
|
+
Enabled: false
|
8
|
+
|
9
|
+
Metrics/AbcSize:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Metrics/CyclomaticComplexity:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
Metrics/BlockLength:
|
16
|
+
Enabled: false
|
17
|
+
|
18
|
+
RSpec/MultipleExpectations:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
Style/ClassAndModuleChildren:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Naming/BinaryOperatorParameterName:
|
25
|
+
Enabled: false
|
26
|
+
|
27
|
+
Metrics/ParameterLists:
|
28
|
+
Enabled: false
|
29
|
+
|
30
|
+
Style/Documentation:
|
31
|
+
Enabled: false
|
32
|
+
|
33
|
+
Naming/UncommunicativeMethodParamName:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
RSpec/ExampleLength:
|
37
|
+
Enabled: false
|
38
|
+
|
39
|
+
Naming/VariableNumber:
|
40
|
+
Enabled: false
|
41
|
+
|
42
|
+
Metrics/MethodLength:
|
43
|
+
Enabled: false
|
44
|
+
|
45
|
+
RSpec/EmptyExampleGroup:
|
46
|
+
Enabled: false
|
47
|
+
|
48
|
+
RSpec/FilePath:
|
49
|
+
Enabled: false
|
50
|
+
|
51
|
+
Lint/UselessAccessModifier:
|
52
|
+
Enabled: false
|
53
|
+
|
54
|
+
Metrics/ModuleLength:
|
55
|
+
Enabled: false
|
56
|
+
|
57
|
+
RSpec/SubjectStub:
|
58
|
+
Enabled: false
|
59
|
+
|
60
|
+
RSpec/MessageSpies:
|
61
|
+
Enabled: false
|
62
|
+
|
63
|
+
RSpec/VerifiedDoubles:
|
64
|
+
Enabled: false
|
65
|
+
|
66
|
+
RSpec/DescribeClass:
|
67
|
+
Enabled: false
|
68
|
+
|
69
|
+
Style/NumericLiterals:
|
70
|
+
Enabled: false
|
71
|
+
|
72
|
+
Naming/MemoizedInstanceVariableName:
|
73
|
+
Enabled: false
|
74
|
+
|
75
|
+
RSpec/LetSetup:
|
76
|
+
Enabled: false
|
77
|
+
|
78
|
+
# Replace some legits include? usages
|
79
|
+
RSpec/PredicateMatcher:
|
80
|
+
Enabled: false
|
81
|
+
|
82
|
+
# For some algo tests we do want to use instance_vars to capture data within algos
|
83
|
+
RSpec/InstanceVariable:
|
84
|
+
Enabled: false
|
85
|
+
|
86
|
+
Style/ModuleFunction:
|
87
|
+
Enabled: false
|
88
|
+
|
89
|
+
Lint/HandleExceptions:
|
90
|
+
Enabled: false
|
91
|
+
|
92
|
+
RSpec/BeforeAfterAll:
|
93
|
+
Enabled: false
|
94
|
+
|
95
|
+
Lint/Loop:
|
96
|
+
Enabled: false
|
97
|
+
|
98
|
+
Style/NumericPredicate:
|
99
|
+
Enabled: false
|
100
|
+
|
101
|
+
Metrics/ClassLength:
|
102
|
+
Enabled: false
|
103
|
+
|
104
|
+
RSpec/NestedGroups:
|
105
|
+
Enabled: false
|
106
|
+
|
107
|
+
Metrics/PerceivedComplexity:
|
108
|
+
Enabled: false
|
109
|
+
|
110
|
+
Style/GuardClause:
|
111
|
+
Enabled: false
|
112
|
+
|
113
|
+
Naming/RescuedExceptionsVariableName:
|
114
|
+
Enabled: false
|
115
|
+
|
116
|
+
Lint/UnusedMethodArgument:
|
117
|
+
AllowUnusedKeywordArguments: true
|
118
|
+
IgnoreEmptyMethods: true
|
119
|
+
|
120
|
+
Lint/NestedMethodDefinition:
|
121
|
+
Enabled: false
|
122
|
+
|
123
|
+
Style/MethodMissingSuper:
|
124
|
+
Enabled: false
|
125
|
+
|
126
|
+
RSpec/MultipleDescribes:
|
127
|
+
Enabled: false
|
128
|
+
|
129
|
+
AllCops:
|
130
|
+
Exclude:
|
131
|
+
- "**/*.sql"
|
132
|
+
- "recipes/**/*"
|
133
|
+
- "db/**/*"
|
134
|
+
- "tmp/**/*"
|
135
|
+
- "vendor/**/*"
|
136
|
+
- "bin/**/*"
|
137
|
+
- "log/**/*"
|
138
|
+
- "ansible/**/*"
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.6.3
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
impersonator (0.1.1)
|
5
|
+
zeitwerk (~> 2.1.6)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
ast (2.4.0)
|
11
|
+
diff-lcs (1.3)
|
12
|
+
jaro_winkler (1.5.2)
|
13
|
+
parallel (1.17.0)
|
14
|
+
parser (2.6.3.0)
|
15
|
+
ast (~> 2.4.0)
|
16
|
+
rainbow (3.0.0)
|
17
|
+
rake (10.5.0)
|
18
|
+
rspec (3.8.0)
|
19
|
+
rspec-core (~> 3.8.0)
|
20
|
+
rspec-expectations (~> 3.8.0)
|
21
|
+
rspec-mocks (~> 3.8.0)
|
22
|
+
rspec-core (3.8.0)
|
23
|
+
rspec-support (~> 3.8.0)
|
24
|
+
rspec-expectations (3.8.3)
|
25
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
26
|
+
rspec-support (~> 3.8.0)
|
27
|
+
rspec-mocks (3.8.0)
|
28
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
29
|
+
rspec-support (~> 3.8.0)
|
30
|
+
rspec-support (3.8.0)
|
31
|
+
rspec_junit_formatter (0.4.1)
|
32
|
+
rspec-core (>= 2, < 4, != 2.12.0)
|
33
|
+
rubocop (0.71.0)
|
34
|
+
jaro_winkler (~> 1.5.1)
|
35
|
+
parallel (~> 1.10)
|
36
|
+
parser (>= 2.6)
|
37
|
+
rainbow (>= 2.2.2, < 4.0)
|
38
|
+
ruby-progressbar (~> 1.7)
|
39
|
+
unicode-display_width (>= 1.4.0, < 1.7)
|
40
|
+
rubocop-rspec (1.33.0)
|
41
|
+
rubocop (>= 0.60.0)
|
42
|
+
ruby-progressbar (1.10.1)
|
43
|
+
unicode-display_width (1.6.0)
|
44
|
+
zeitwerk (2.1.6)
|
45
|
+
|
46
|
+
PLATFORMS
|
47
|
+
ruby
|
48
|
+
|
49
|
+
DEPENDENCIES
|
50
|
+
bundler (~> 1.17)
|
51
|
+
impersonator!
|
52
|
+
rake (~> 10.0)
|
53
|
+
rspec (~> 3.0)
|
54
|
+
rspec_junit_formatter
|
55
|
+
rubocop
|
56
|
+
rubocop-rspec
|
57
|
+
|
58
|
+
BUNDLED WITH
|
59
|
+
1.17.2
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 Jorge Manrubia
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
[![CircleCI](https://circleci.com/gh/jorgemanrubia/impersonator.svg?style=svg)](https://circleci.com/gh/jorgemanrubia/impersonator)
|
2
|
+
|
3
|
+
# Impersonator
|
4
|
+
|
5
|
+
Impersonator is a Ruby library to record and replay object interactions.
|
6
|
+
|
7
|
+
When testing, you often find services that are expensive to invoke, and you need to use a [double](https://martinfowler.com/bliki/TestDouble.html) instead. Creating stubs and mocks for simple scenarios is easy, but, for complex interactions, things get messy fast. Stubbing elaborated canned response and orchestrating multiple expectations quickly degenerates in brittle tests that are hard to write and maintain.
|
8
|
+
|
9
|
+
Impersonator comes to the rescue. Given an object and a list of methods to impersonate:
|
10
|
+
|
11
|
+
- The first time each method is invoked, it will record its invocations, including passed arguments, return values, and yielded values. This is known as *record mode*.
|
12
|
+
- The next times, it will reproduce the recorded values and will validate that the method was invoked with the same arguments, in a specific order and the exact number of times. This is known as *replay mode*.
|
13
|
+
|
14
|
+
Impersonator only focuses on validating invocation signature and reproducing output values, which is perfect for many services. It won't work for services that trigger additional logic that is relevant to the test (e.g., if the method sends an email, the impersonated method won't send it).
|
15
|
+
|
16
|
+
Familiar with [VCR](https://github.com/vcr/vcr)? Impersonator is like VCR but for ruby objects instead of HTTP.
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Add this line to your application's Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'impersonator', group: :test
|
24
|
+
```
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
|
28
|
+
$ bundle
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
Use `Impersonator.impersonate` passing in a list of methods to impersonate and a block that will instantiate the object at record time:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
Impersonator.impersonate(:add, :divide) { Calculator.new }
|
36
|
+
```
|
37
|
+
|
38
|
+
* At record time, `Calculator` will be instantiated and their methods normally invoked, recording the returned values (and yielded values if any).
|
39
|
+
* At replay time, `Calculator` won't be instantiated. Instead, a double object will be generated on the fly that will replay the recorded values.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
class Calculator
|
43
|
+
def add(number_1, number_2)
|
44
|
+
number_1 + number_2
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# The first time it records...
|
49
|
+
Impersonator.recording('calculator add') do
|
50
|
+
impersonated_calculator = Impersonator.impersonate(:add) { Calculator.new }
|
51
|
+
puts impersonated_calculator.add(2, 3) # 5
|
52
|
+
end
|
53
|
+
|
54
|
+
# The next time it replays
|
55
|
+
Object.send :remove_const, :Calculator # Calculator does not even have to exist now
|
56
|
+
Impersonator.recording('calculator add') do
|
57
|
+
impersonated_calculator = Impersonator.impersonate(:add) { Calculator.new }
|
58
|
+
puts impersonated_calculator.add(2, 3) # 5
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
Typically you will use `impersonate` for testing, so this is how your test will look:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
# The second time the test runs, impersonator will replay the
|
66
|
+
# recorded results
|
67
|
+
test 'sums the numbers' do
|
68
|
+
Impersonator.recording('calculator add') do
|
69
|
+
calculator = Impersonator.impersonate(:add){ Calculator.new }
|
70
|
+
assert_equal 5, calculator.add(2, 3)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
Impersonated methods will record and replay:
|
76
|
+
|
77
|
+
- Arguments
|
78
|
+
- Return values
|
79
|
+
- Yielded values
|
80
|
+
|
81
|
+
### Impersonate certain methods only
|
82
|
+
|
83
|
+
Use `Impersonator#impersonate_methods` to impersonate certain methods only. At replay time, the impersonated object will delegate to the actual object all the methods except the impersonated ones.
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
actual_calculator = Calculator.new
|
87
|
+
impersonator = Impersonator.impersonate(actual_calculator, :add)
|
88
|
+
```
|
89
|
+
|
90
|
+
In this case, in replay mode, `Calculator` gets instantiated normally and any method other than `#add` will be delegated to `actual_calculator`.
|
91
|
+
|
92
|
+
## Configuration
|
93
|
+
|
94
|
+
### Recordings path
|
95
|
+
|
96
|
+
`Impersonator` works by recording method invocations in `YAML` format. By default, recordings are saved in:
|
97
|
+
|
98
|
+
- `spec/recordings` if a `spec` folder is present in the project
|
99
|
+
- `test/recordings` otherwise
|
100
|
+
|
101
|
+
You can configure this path with:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
Impersonator.configure do |config|
|
105
|
+
config.recordings_path = 'my/own/recording/path'
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
### Ignore arguments when matching methods
|
110
|
+
|
111
|
+
By default, to determine if a method invocation was right, the list of arguments will be matched with `==`. You can configure how this work by providing a list of argument indexes to ignore.
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
impersonator = Impersonator.impersonate(:add){ Test::Calculator.new }
|
115
|
+
impersonator.configure_method_matching_for(:add) do |config|
|
116
|
+
config.ignore_arguments_at 0
|
117
|
+
end
|
118
|
+
|
119
|
+
# Now the first parameter of #add will be ignored.
|
120
|
+
#
|
121
|
+
# In record mode:
|
122
|
+
impersonator.add(1, 2) # 3
|
123
|
+
|
124
|
+
# In replay mode
|
125
|
+
impersonator.add(9999, 2) # will still return 3 and won't fail because the first argument is ignored
|
126
|
+
```
|
127
|
+
|
128
|
+
### Disabling record mode
|
129
|
+
|
130
|
+
You can disable `impersonator` by passing `disable: true` to `Impersonator.recording`:
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
Impersonator.recording('test recording', disabled: true) do
|
134
|
+
# ...
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
138
|
+
This will effectively force record mode at all times. This is handy while you are figuring out how interactions with the mocked service go. It will save the recordings, but it will never use them.
|
139
|
+
|
140
|
+
### Configuring attributes to serialize
|
141
|
+
|
142
|
+
`Impersonator` relies on Ruby standard `YAML` library for serializing/deserializing data. It works with simple attributes, arrays, hashes and objects which attributes are serializable in a recurring way. This means that you don't have to care when interchanging value objects, which is a common scenario when impersonating RPC-like clients.
|
143
|
+
|
144
|
+
However, there are some types, like `Proc`, anonymous classes, or `IO` classes like `File`, that will make the serialization process fail. You can customize which attributes are serialized by overriding `init_with` and `encode_with` in the class you want to serialize. You will typically exclude the problematic attributes by including only the compatible ones.
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
class MyClass
|
148
|
+
# ...
|
149
|
+
|
150
|
+
def init_with(coder)
|
151
|
+
self.name = coder['name']
|
152
|
+
end
|
153
|
+
|
154
|
+
def encode_with(coder)
|
155
|
+
coder['name'] = name
|
156
|
+
end
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
### RSpec configuration
|
161
|
+
|
162
|
+
`Impersonator` is test-framework agnostic. If you are using [RSpec](https://rspec.info), you can configure an `around` hook that will start a recording session automatically for each example that has an `impersonator` tag:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
RSpec.configure do |config|
|
166
|
+
config.around(:example, :impersonator) do |example|
|
167
|
+
Impersonator.recording(example.full_description) do
|
168
|
+
example.run
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
Now you can just tag your tests with `impersonator` and an implicit recording named after the example will be available automatically, so you don't have to invoke `Impersonator.recording` anymore.
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
describe Calculator, :impersonator do
|
178
|
+
it 'sums numbers' do
|
179
|
+
# there is an implicit recording stored in 'calculator-sums-numbers.yaml'
|
180
|
+
impersonator = Impersonator.impersonate(:add){ Calculator.new }
|
181
|
+
expect(impersonator.add(1, 2)).to eq(3)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
```
|
185
|
+
|
186
|
+
## Thanks
|
187
|
+
|
188
|
+
- This library was heavily inspired by [VCR](https://github.com/vcr/vcr). A gem that blew my mind years ago and that has been in my toolbox since then.
|
189
|
+
|
190
|
+
## Links
|
191
|
+
|
192
|
+
- [Blog post](https://www.jorgemanrubia.com/2019/06/16/impersonator-a-ruby-library-to-record-and-replay-object-interactions/)
|
193
|
+
|
194
|
+
## Contributing
|
195
|
+
|
196
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jorgemanrubia/impersonator.
|
197
|
+
|
198
|
+
## License
|
199
|
+
|
200
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).`
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "impersonator"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'impersonator/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'impersonator'
|
7
|
+
spec.version = Impersonator::VERSION
|
8
|
+
spec.authors = ['Jorge Manrubia']
|
9
|
+
spec.email = ['jorge.manrubia@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'Generate test stubs that replay recorded interactions'
|
12
|
+
spec.description = 'Record and replay object interactions. Ideal for mocking not-http services when testing (just because, for http, VCR is probably what you want)'
|
13
|
+
spec.homepage = 'https://github.com/jorgemanrubia/impersonator'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
17
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
18
|
+
if spec.respond_to?(:metadata)
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
20
|
+
spec.metadata['source_code_uri'] = 'https://github.com/jorgemanrubia/impersonator'
|
21
|
+
else
|
22
|
+
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
23
|
+
'public gem pushes.'
|
24
|
+
end
|
25
|
+
|
26
|
+
# Specify which files should be added to the gem when it is released.
|
27
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
28
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
29
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
30
|
+
end
|
31
|
+
spec.bindir = 'exe'
|
32
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
33
|
+
spec.require_paths = ['lib']
|
34
|
+
|
35
|
+
spec.add_runtime_dependency 'zeitwerk', '~> 2.1.6'
|
36
|
+
spec.add_development_dependency 'bundler', '~> 1.17'
|
37
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
38
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
39
|
+
spec.add_development_dependency 'rspec_junit_formatter'
|
40
|
+
end
|
data/lib/impersonator.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'impersonator/version'
|
2
|
+
|
3
|
+
require 'zeitwerk'
|
4
|
+
require 'logger'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
loader = Zeitwerk::Loader.for_gem
|
9
|
+
loader.setup
|
10
|
+
|
11
|
+
module Impersonator
|
12
|
+
extend Api
|
13
|
+
|
14
|
+
def self.logger
|
15
|
+
@logger ||= ::Logger.new(STDOUT).tap do |logger|
|
16
|
+
logger.level = Logger::WARN
|
17
|
+
logger.datetime_format = '%Y-%m-%d %H:%M:%S'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Impersonator
|
2
|
+
module Api
|
3
|
+
def recording(label, disabled: false)
|
4
|
+
@current_recording = ::Impersonator::Recording.new(label, disabled: disabled, recordings_path: configuration.recordings_path)
|
5
|
+
@current_recording.start
|
6
|
+
yield
|
7
|
+
@current_recording.finish
|
8
|
+
ensure
|
9
|
+
@current_recording = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def current_recording
|
13
|
+
@current_recording
|
14
|
+
end
|
15
|
+
|
16
|
+
def configure
|
17
|
+
yield configuration
|
18
|
+
end
|
19
|
+
|
20
|
+
def configuration
|
21
|
+
@configuration ||= Configuration.new
|
22
|
+
end
|
23
|
+
|
24
|
+
# Reset configuration and other global state.
|
25
|
+
#
|
26
|
+
# It is meant to be used internally by tests.
|
27
|
+
def reset
|
28
|
+
@current_recording = nil
|
29
|
+
@configuration = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
# Receives a list of methods to impersonate and a block that will be used, at record time, to
|
33
|
+
# instantiate the object to impersonate. At replay time, it will generate a double that will
|
34
|
+
# replay the methods.
|
35
|
+
#
|
36
|
+
# impersonator = Impersonator.impersonate(:add, :subtract) { Calculator.new }
|
37
|
+
# impersonator.add(3, 4)
|
38
|
+
#
|
39
|
+
# Notice that the actual object won't be instantiated in record mode. For that reason, the impersonated
|
40
|
+
# object will only respond to the list of impersonated methods.
|
41
|
+
#
|
42
|
+
# If you need to invoke other (not impersonated) methods see #impersonate_method instead.
|
43
|
+
#
|
44
|
+
# @return [Object] the impersonated object
|
45
|
+
def impersonate(*methods)
|
46
|
+
raise ArgumentError, 'Provide a block to instantiate the object to impersonate in record mode' unless block_given?
|
47
|
+
|
48
|
+
object_to_impersonate = if current_recording&.record_mode?
|
49
|
+
yield
|
50
|
+
else
|
51
|
+
Double.new(*methods)
|
52
|
+
end
|
53
|
+
impersonate_methods(object_to_impersonate, *methods)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Impersonates a list of methods of a given object
|
57
|
+
#
|
58
|
+
# The returned object will impersonate the list of methods and will delegate the rest of method calls
|
59
|
+
# to the actual object.
|
60
|
+
#
|
61
|
+
# @return [Object] the impersonated object
|
62
|
+
def impersonate_methods(actual_object, *methods)
|
63
|
+
raise Impersonator::Errors::ConfigurationError, 'You must start a recording to impersonate objects. Use Impersonator.recording {}' unless @current_recording
|
64
|
+
|
65
|
+
::Impersonator::Proxy.new(actual_object, recording: current_recording, impersonated_methods: methods)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Impersonator
|
2
|
+
BlockSpy = Struct.new(:block_invocations, :actual_block, keyword_init: true) do
|
3
|
+
def block
|
4
|
+
@block ||= proc do |*arguments|
|
5
|
+
self.block_invocations ||= []
|
6
|
+
self.block_invocations << BlockInvocation.new(arguments: arguments)
|
7
|
+
return_value = actual_block.call(*arguments)
|
8
|
+
return_value
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def init_with(coder)
|
13
|
+
self.block_invocations = coder['block_invocations']
|
14
|
+
end
|
15
|
+
|
16
|
+
def encode_with(coder)
|
17
|
+
coder['block_invocations'] = block_invocations
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Impersonator
|
2
|
+
Configuration = Struct.new(:recordings_path, keyword_init: true) do
|
3
|
+
DEFAULT_RECORDINGS_FOLDER = 'recordings'.freeze
|
4
|
+
|
5
|
+
def initialize(*)
|
6
|
+
super
|
7
|
+
self.recordings_path ||= detect_default_recordings_path
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def detect_default_recordings_path
|
13
|
+
base_path = File.exist?('spec') ? 'spec' : 'test'
|
14
|
+
File.join(base_path, DEFAULT_RECORDINGS_FOLDER)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Impersonator
|
2
|
+
Method = Struct.new(:name, :arguments, :block, :matching_configuration, keyword_init: true) do
|
3
|
+
def to_s
|
4
|
+
string = name.to_s
|
5
|
+
|
6
|
+
arguments_string = arguments&.collect(&:to_s)&.join(', ')
|
7
|
+
|
8
|
+
string << "(#{arguments_string})"
|
9
|
+
string << ' {with block}' if block
|
10
|
+
string
|
11
|
+
end
|
12
|
+
|
13
|
+
def block_spy
|
14
|
+
return nil if !@block_spy && !block
|
15
|
+
|
16
|
+
@block_spy ||= BlockSpy.new(actual_block: block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def init_with(coder)
|
20
|
+
self.name = coder['name']
|
21
|
+
self.arguments = coder['arguments']
|
22
|
+
self.matching_configuration = coder['matching_configuration']
|
23
|
+
@block_spy = coder['block_spy']
|
24
|
+
end
|
25
|
+
|
26
|
+
def encode_with(coder)
|
27
|
+
coder['name'] = name
|
28
|
+
coder['arguments'] = arguments
|
29
|
+
coder['block_spy'] = block_spy
|
30
|
+
coder['matching_configuration'] = matching_configuration
|
31
|
+
end
|
32
|
+
|
33
|
+
def ==(other_method)
|
34
|
+
my_arguments = arguments.dup
|
35
|
+
other_arguments = other_method.arguments.dup
|
36
|
+
matching_configuration&.ignored_positions&.each do |ignored_position|
|
37
|
+
my_arguments.delete_at(ignored_position)
|
38
|
+
other_arguments.delete_at(ignored_position)
|
39
|
+
end
|
40
|
+
|
41
|
+
name == other_method.name && my_arguments == other_arguments && !block_spy == !other_method.block_spy
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Impersonator
|
2
|
+
class Proxy
|
3
|
+
include HasLogger
|
4
|
+
|
5
|
+
attr_reader :impersonated_object
|
6
|
+
|
7
|
+
def initialize(impersonated_object, recording:, impersonated_methods:)
|
8
|
+
validate_object_has_methods_to_impersonate!(impersonated_object, impersonated_methods)
|
9
|
+
|
10
|
+
@impersonated_object = impersonated_object
|
11
|
+
@impersonated_methods = impersonated_methods.collect(&:to_sym)
|
12
|
+
@recording = recording
|
13
|
+
@method_matching_configurations_by_method = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(method_name, *args, &block)
|
17
|
+
if @impersonated_methods.include?(method_name.to_sym)
|
18
|
+
invoke_impersonated_method(method_name, *args, &block)
|
19
|
+
else
|
20
|
+
@impersonated_object.send(method_name, *args, &block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def respond_to_missing?(method_name, *args)
|
25
|
+
impersonated_object.respond_to_missing?(method_name, *args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def configure_method_matching_for(method)
|
29
|
+
method_matching_configurations_by_method[method.to_sym] ||= MethodMatchingConfiguration.new
|
30
|
+
yield method_matching_configurations_by_method[method]
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :recording, :impersonated_methods, :method_matching_configurations_by_method
|
36
|
+
|
37
|
+
def validate_object_has_methods_to_impersonate!(object, methods_to_impersonate)
|
38
|
+
missing_methods = methods_to_impersonate.find_all do |method|
|
39
|
+
!object.respond_to?(method.to_sym)
|
40
|
+
end
|
41
|
+
|
42
|
+
raise Impersonator::Errors::ConfigurationError, "These methods to impersonate does not exist: #{missing_methods.inspect}" unless missing_methods.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
def invoke_impersonated_method(method_name, *args, &block)
|
46
|
+
method = Method.new(name: method_name, arguments: args, block: block, matching_configuration: method_matching_configurations_by_method[method_name.to_sym])
|
47
|
+
if recording.replay_mode?
|
48
|
+
recording.replay(method)
|
49
|
+
else
|
50
|
+
@impersonated_object.send(method_name, *args, &method&.block_spy&.block).tap do |return_value|
|
51
|
+
recording.record(method, return_value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Impersonator
|
4
|
+
class Recording
|
5
|
+
include HasLogger
|
6
|
+
|
7
|
+
attr_reader :label
|
8
|
+
|
9
|
+
def initialize(label, disabled: false, recordings_path:)
|
10
|
+
@label = label
|
11
|
+
@recordings_path = recordings_path
|
12
|
+
@disabled = disabled
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
logger.debug "Starting recording #{label}..."
|
17
|
+
if can_replay?
|
18
|
+
start_in_replay_mode
|
19
|
+
else
|
20
|
+
start_in_record_mode
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def record(method, return_value)
|
25
|
+
method_invocation = MethodInvocation.new(method_instance: method, return_value: return_value)
|
26
|
+
|
27
|
+
@method_invocations << method_invocation
|
28
|
+
end
|
29
|
+
|
30
|
+
def replay(method)
|
31
|
+
method_invocation = @method_invocations.shift
|
32
|
+
raise Impersonator::Errors::MethodInvocationError, "Unexpected method invocation received: #{method}" unless method_invocation
|
33
|
+
|
34
|
+
validate_method_signature!(method, method_invocation.method_instance)
|
35
|
+
replay_block(method_invocation, method)
|
36
|
+
|
37
|
+
method_invocation.return_value
|
38
|
+
end
|
39
|
+
|
40
|
+
def finish
|
41
|
+
logger.debug "Recording #{label} finished"
|
42
|
+
if record_mode?
|
43
|
+
finish_in_record_mode
|
44
|
+
else
|
45
|
+
finish_in_replay_mode
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def replay_mode?
|
50
|
+
@replay_mode
|
51
|
+
end
|
52
|
+
|
53
|
+
def record_mode?
|
54
|
+
!replay_mode?
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def can_replay?
|
60
|
+
!@disabled && File.exist?(file_path)
|
61
|
+
end
|
62
|
+
|
63
|
+
def replay_block(recorded_method_invocation, method_to_replay)
|
64
|
+
block_spy = recorded_method_invocation.method_instance.block_spy
|
65
|
+
block_spy&.block_invocations&.each do |block_invocation|
|
66
|
+
method_to_replay.block.call(*block_invocation.arguments)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def start_in_replay_mode
|
71
|
+
logger.debug 'Replay mode'
|
72
|
+
@replay_mode = true
|
73
|
+
@method_invocations = YAML.load_file(file_path)
|
74
|
+
end
|
75
|
+
|
76
|
+
def start_in_record_mode
|
77
|
+
logger.debug 'Recording mode'
|
78
|
+
@replay_mode = false
|
79
|
+
make_sure_recordings_dir_exists
|
80
|
+
@method_invocations = []
|
81
|
+
end
|
82
|
+
|
83
|
+
def finish_in_record_mode
|
84
|
+
File.open(file_path, 'w') do |file|
|
85
|
+
YAML.dump(@method_invocations, file)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def finish_in_replay_mode
|
90
|
+
unless @method_invocations.empty?
|
91
|
+
raise Impersonator::Errors::MethodInvocationError, "Expecting #{@method_invocations.length} method invocations"\
|
92
|
+
" that didn't happen: #{@method_invocations.inspect}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def file_path
|
97
|
+
File.join(@recordings_path, "#{label_as_file_name}.yml")
|
98
|
+
end
|
99
|
+
|
100
|
+
def label_as_file_name
|
101
|
+
label.downcase.gsub(/[\(\)\s \#:]/, '-').gsub(/[\-]+/, '-').gsub(/(^-)|(-$)/, '')
|
102
|
+
end
|
103
|
+
|
104
|
+
def make_sure_recordings_dir_exists
|
105
|
+
dirname = File.dirname(file_path)
|
106
|
+
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
|
107
|
+
end
|
108
|
+
|
109
|
+
def validate_method_signature!(expected_method, actual_method)
|
110
|
+
unless actual_method == expected_method
|
111
|
+
raise Impersonator::Errors::MethodInvocationError, <<~ERROR
|
112
|
+
Expecting:
|
113
|
+
#{expected_method}
|
114
|
+
But received:
|
115
|
+
#{actual_method}
|
116
|
+
ERROR
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: impersonator
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jorge Manrubia
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-06-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: zeitwerk
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.1.6
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.1.6
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.17'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.17'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec_junit_formatter
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Record and replay object interactions. Ideal for mocking not-http services
|
84
|
+
when testing (just because, for http, VCR is probably what you want)
|
85
|
+
email:
|
86
|
+
- jorge.manrubia@gmail.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".circleci/config.yml"
|
92
|
+
- ".gitignore"
|
93
|
+
- ".rspec"
|
94
|
+
- ".rubocop.yml"
|
95
|
+
- ".ruby-version"
|
96
|
+
- ".travis.yml"
|
97
|
+
- Gemfile
|
98
|
+
- Gemfile.lock
|
99
|
+
- LICENSE.txt
|
100
|
+
- README.md
|
101
|
+
- Rakefile
|
102
|
+
- bin/console
|
103
|
+
- bin/setup
|
104
|
+
- impersonator.gemspec
|
105
|
+
- lib/impersonator.rb
|
106
|
+
- lib/impersonator/api.rb
|
107
|
+
- lib/impersonator/block_invocation.rb
|
108
|
+
- lib/impersonator/block_spy.rb
|
109
|
+
- lib/impersonator/configuration.rb
|
110
|
+
- lib/impersonator/double.rb
|
111
|
+
- lib/impersonator/errors/configuration_error.rb
|
112
|
+
- lib/impersonator/errors/method_invocation_error.rb
|
113
|
+
- lib/impersonator/has_logger.rb
|
114
|
+
- lib/impersonator/method.rb
|
115
|
+
- lib/impersonator/method_invocation.rb
|
116
|
+
- lib/impersonator/method_matching_configuration.rb
|
117
|
+
- lib/impersonator/proxy.rb
|
118
|
+
- lib/impersonator/recording.rb
|
119
|
+
- lib/impersonator/version.rb
|
120
|
+
homepage: https://github.com/jorgemanrubia/impersonator
|
121
|
+
licenses:
|
122
|
+
- MIT
|
123
|
+
metadata:
|
124
|
+
homepage_uri: https://github.com/jorgemanrubia/impersonator
|
125
|
+
source_code_uri: https://github.com/jorgemanrubia/impersonator
|
126
|
+
post_install_message:
|
127
|
+
rdoc_options: []
|
128
|
+
require_paths:
|
129
|
+
- lib
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
requirements: []
|
141
|
+
rubygems_version: 3.0.3
|
142
|
+
signing_key:
|
143
|
+
specification_version: 4
|
144
|
+
summary: Generate test stubs that replay recorded interactions
|
145
|
+
test_files: []
|