compact 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 +8 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +75 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +30 -0
- data/LICENSE.txt +21 -0
- data/README.md +301 -0
- data/Rakefile +24 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/compact.gemspec +36 -0
- data/lib/compact.rb +55 -0
- data/lib/compact/argument_interceptor.rb +23 -0
- data/lib/compact/contract.rb +104 -0
- data/lib/compact/invocation.rb +37 -0
- data/lib/compact/ledger.rb +64 -0
- data/lib/compact/verification_codes.rb +6 -0
- data/lib/compact/version.rb +3 -0
- data/lib/minitest/compact_plugin.rb +9 -0
- data/lib/minitest/compact_reporter.rb +11 -0
- metadata +139 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e745ded7faf0e81a41da4cbf2f34c819e470a0eb
|
4
|
+
data.tar.gz: 48c7298e8e94f77d0bf1f3c393babe9153d4cda8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e05392644bf09d4c61199c702a957a023ab5cc293ac7dd50c34d66d8590c3a0c280f936f934a7417f56d54826416f7ff9bc2e9a0f64b90f3ffe0683e3d55b48d
|
7
|
+
data.tar.gz: 749f4b24a836dcac3e77badbe97867cab45a354b5a1511f47657e203a97115abc8dedf8844252756aa1e22be473a82252d0ccf51808917495bd71331462e90e6
|
data/.gitignore
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
compact
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.5.0
|
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
TL;DR: MINSWAN.
|
3
|
+
|
4
|
+
## Our Pledge
|
5
|
+
|
6
|
+
In the interest of fostering an open and welcoming environment, we as
|
7
|
+
contributors and maintainers pledge to making participation in our project and
|
8
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
9
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
10
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
11
|
+
orientation.
|
12
|
+
|
13
|
+
## Our Standards
|
14
|
+
|
15
|
+
Examples of behavior that contributes to creating a positive environment
|
16
|
+
include:
|
17
|
+
|
18
|
+
* Using welcoming and inclusive language
|
19
|
+
* Being respectful of differing viewpoints and experiences
|
20
|
+
* Gracefully accepting constructive criticism
|
21
|
+
* Focusing on what is best for the community
|
22
|
+
* Showing empathy towards other community members
|
23
|
+
|
24
|
+
Examples of unacceptable behavior by participants include:
|
25
|
+
|
26
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
27
|
+
advances
|
28
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
29
|
+
* Public or private harassment
|
30
|
+
* Publishing others' private information, such as a physical or electronic
|
31
|
+
address, without explicit permission
|
32
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
33
|
+
professional setting
|
34
|
+
|
35
|
+
## Our Responsibilities
|
36
|
+
|
37
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
38
|
+
behavior and are expected to take appropriate and fair corrective action in
|
39
|
+
response to any instances of unacceptable behavior.
|
40
|
+
|
41
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
42
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
43
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
44
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
45
|
+
threatening, offensive, or harmful.
|
46
|
+
|
47
|
+
## Scope
|
48
|
+
|
49
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
50
|
+
when an individual is representing the project or its community. Examples of
|
51
|
+
representing a project or community include using an official project e-mail
|
52
|
+
address, posting via an official social media account, or acting as an appointed
|
53
|
+
representative at an online or offline event. Representation of a project may be
|
54
|
+
further defined and clarified by project maintainers.
|
55
|
+
|
56
|
+
## Enforcement
|
57
|
+
|
58
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
59
|
+
reported by contacting the project team at rold9888@gmail.com. All
|
60
|
+
complaints will be reviewed and investigated and will result in a response that
|
61
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
62
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
63
|
+
Further details of specific enforcement policies may be posted separately.
|
64
|
+
|
65
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
66
|
+
faith may face temporary or permanent repercussions as determined by other
|
67
|
+
members of the project's leadership.
|
68
|
+
|
69
|
+
## Attribution
|
70
|
+
|
71
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
72
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
73
|
+
|
74
|
+
[homepage]: http://contributor-covenant.org
|
75
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
compact (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
docile (1.3.2)
|
10
|
+
minitest (5.14.0)
|
11
|
+
mocha (1.11.2)
|
12
|
+
rake (13.0.1)
|
13
|
+
simplecov (0.18.5)
|
14
|
+
docile (~> 1.1)
|
15
|
+
simplecov-html (~> 0.11)
|
16
|
+
simplecov-html (0.12.2)
|
17
|
+
|
18
|
+
PLATFORMS
|
19
|
+
ruby
|
20
|
+
|
21
|
+
DEPENDENCIES
|
22
|
+
bundler (~> 2.0)
|
23
|
+
compact!
|
24
|
+
minitest (~> 5.0)
|
25
|
+
mocha (~> 1.11)
|
26
|
+
rake (~> 13.0)
|
27
|
+
simplecov (~> 0.18)
|
28
|
+
|
29
|
+
BUNDLED WITH
|
30
|
+
2.0.2
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 Rob
|
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,301 @@
|
|
1
|
+
# COMPACT: COMPrehensive Automated Contract Testing
|
2
|
+
|
3
|
+
|
4
|
+
>noun: __compact__
|
5
|
+
>
|
6
|
+
> *a formal agreement or contract between two or more parties.*
|
7
|
+
|
8
|
+
_Because lots of the PACT - based names were taken_
|
9
|
+
## Motivation
|
10
|
+
This library aims to help you combat the problem of drifting test doubles:
|
11
|
+
if you change the behaviour of a class upon which other classes depend, and
|
12
|
+
you unit test those classes using test doubles in place of the real thing,
|
13
|
+
then you run the risk of testing your classes against the old behaviour of
|
14
|
+
their dependencies. (Note that this is distinct from the idea of contract tests
|
15
|
+
against remote services.)
|
16
|
+
|
17
|
+
The traditional approach to guarding against this problem is to complement
|
18
|
+
unit tests with integration tests. But Joe Rainsberger would have us believe
|
19
|
+
that
|
20
|
+
[integration tests are a scam](https://www.infoq.com/presentations/integration-tests-scam/),
|
21
|
+
and that there are real advantages to verifying the basic correctness of our code with
|
22
|
+
[contract tests](https://blog.thecodewhisperer.com/permalink/getting-started-with-contract-tests) instead.
|
23
|
+
|
24
|
+
To take a concrete example, let's think about a `Calculator` class that delegates
|
25
|
+
responsibility for individual arithmetic operations to classes such as `Adder`. A
|
26
|
+
collaboration test looks like
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class CalculatorTest < MiniTest::Test
|
30
|
+
def test_addition_collaboration
|
31
|
+
adder = Minitest::Mock.new
|
32
|
+
adder.expect(:add,3,[1,2])
|
33
|
+
subject = Calculator.new(adder, *other_dependencies)
|
34
|
+
assert_equal 3, subject.add(1,2)
|
35
|
+
end
|
36
|
+
#...
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
To ensure that the expected behaviour of our mock reflects the actual behaviour of our
|
41
|
+
code, we write a corresponding contract test for the `Adder` class:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
class AdderTest < MiniTest::Test
|
45
|
+
def test_addition_contract
|
46
|
+
subject = Adder.new
|
47
|
+
assert_equal 3, subject.add(1,2)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
Note the correspondence between the supplied arguments and the return values of the `Adder#add`
|
53
|
+
method in these tests.
|
54
|
+
|
55
|
+
Maintaining this correspondence by hand requires discipline. It becomes significantly harder on
|
56
|
+
a team of developers. This gem helps to seek automate maintaining this correspondence.
|
57
|
+
|
58
|
+
## Design goals
|
59
|
+
#### Library agnostic
|
60
|
+
Whereas some previous steps in this direction have been integrated into specific testing/mocking
|
61
|
+
libraries such as
|
62
|
+
[Bogus](https://relishapp.com/bogus/bogus/v/0-1-6/docs/contract-tests),
|
63
|
+
this aims to be something you can drop into an existing test suite and make it immediately more
|
64
|
+
robust, whatever test suite or test double library you are using.
|
65
|
+
(We're not there yet: see below for the current status.)
|
66
|
+
|
67
|
+
#### Mock roles, not objects
|
68
|
+
The correspondence between test doubles and objects that fulfill those roles is to be labelled by the
|
69
|
+
programmer, rather than tied to specific classes.
|
70
|
+
|
71
|
+
#### Verify behaviour, not just syntax
|
72
|
+
It also aims to go one step further than most other such libraries in trying to maintain the
|
73
|
+
correspondence between inputs and return values, whereas something like
|
74
|
+
[RSpec's verifying doubles](https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles)
|
75
|
+
just verify that a stubbed method is present on some class.
|
76
|
+
|
77
|
+
## Usage
|
78
|
+
To record the contracts defined by a test double, prepare the double as you normally would but
|
79
|
+
inside a block passed to `Compact.prepare_double(role_name)`:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
class CalculatorTest < MiniTest::Test
|
83
|
+
def test_addition_collaboration
|
84
|
+
adder = Compact.prepare_double('adder') do
|
85
|
+
mock = Minitest::Mock.new
|
86
|
+
mock.expect(:add,3,[1,2])
|
87
|
+
mock
|
88
|
+
end
|
89
|
+
subject = Calculator.new(adder, *other_dependencies)
|
90
|
+
assert_equal 3, subject.add(1,2)
|
91
|
+
end
|
92
|
+
#...
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
This method wraps the return value of the block with a simple decorator that tracks the methods
|
97
|
+
dispatched to that double, the arguments with which they are invoked, and the returned values.
|
98
|
+
It stores these in some state that persists across test runs to produce a summary report at the
|
99
|
+
end of your test run.
|
100
|
+
|
101
|
+
The corresponding enhancement to the contract test is achieved by the method
|
102
|
+
`Compact.verify_contract(role_name, object_that_fulfills_role)`. Passing a block to this method in which
|
103
|
+
a stubbed method is invoked with the appropriate arguments and returns the correct value verifies
|
104
|
+
the contract.
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
class AdderTest < MiniTest::Test
|
108
|
+
def test_addition_contract
|
109
|
+
adder = Adder.new
|
110
|
+
Compact.verify_contract('adder', adder){|adder| assert_equal 3, adder.add(1,2) }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
A contract can fail to be verified in three ways:
|
116
|
+
|
117
|
+
1. A missing contract test
|
118
|
+
2. A verified method not being asserted by some test_double.
|
119
|
+
3. A mismatch in the behaviour of a double and real object intended to fulfill its role.
|
120
|
+
|
121
|
+
At the end of your test run Compact will alert you to any instances of all three of these
|
122
|
+
cases. See the next section for an example.
|
123
|
+
|
124
|
+
## A complete example
|
125
|
+
An executable version of this can be found in `/examples`, and run with `rake examples`.
|
126
|
+
Note that the contract tests would normally be written against the classes such as `Adder`
|
127
|
+
etc. but are interleaved with the collaboration tests in this example to highlight the
|
128
|
+
correspondences.
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
require "compact"
|
132
|
+
|
133
|
+
# The calculator class delegates its difficult arithmetic
|
134
|
+
# work to four service classes.
|
135
|
+
#
|
136
|
+
# Addition provides a happy example of contract validation.
|
137
|
+
# Subtraction has a collaboration test without a contract test.
|
138
|
+
# Multiplication is a collaborator in search of a collaboration.
|
139
|
+
# Division tells the unhappy story of a soul who's confused about
|
140
|
+
# whether they want integer or floating point division.
|
141
|
+
class CalculatorTest < MiniTest::Test
|
142
|
+
|
143
|
+
def test_addition_collaboration
|
144
|
+
adder = Compact.prepare_double('adder') do
|
145
|
+
adder = Minitest::Mock.new
|
146
|
+
adder.expect(:add,3,[1,2])
|
147
|
+
end
|
148
|
+
|
149
|
+
subject = Calculator.new(adder, nil, nil, nil)
|
150
|
+
subject.add(1,2)
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_addition_contract
|
154
|
+
adder = Adder.new
|
155
|
+
Compact.verify_contract('adder', adder){|adder| assert_equal 3, adder.add(1,2) }
|
156
|
+
end
|
157
|
+
|
158
|
+
# NO subtraction contract test!
|
159
|
+
def test_subtraction_collaboration
|
160
|
+
subtracter = Compact.prepare_double('subtracter') do
|
161
|
+
subtracter = Minitest::Mock.new
|
162
|
+
subtracter.expect(:subtract,5,[7,2])
|
163
|
+
end
|
164
|
+
|
165
|
+
subject = Calculator.new(nil, subtracter, nil, nil)
|
166
|
+
subject.subtract(7,2)
|
167
|
+
end
|
168
|
+
|
169
|
+
# NO multiplication collaboration test!
|
170
|
+
def test_multiplication_contract
|
171
|
+
multiplier = Multiplier.new
|
172
|
+
Compact.verify_contract('multiplier', multiplier) do |multiplier|
|
173
|
+
assert_equal 6, multiplier.multiply(2,3)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def test_division_collaboration
|
178
|
+
divider = Compact.prepare_double('divider') do
|
179
|
+
mock = Minitest::Mock.new
|
180
|
+
mock.expect(:divide,2.5,[5,2])
|
181
|
+
end
|
182
|
+
|
183
|
+
subject = Calculator.new(nil, nil, nil, divider)
|
184
|
+
subject.divide(5,2)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Mismatched assertion
|
188
|
+
def test_division_contract
|
189
|
+
divider = Divider.new
|
190
|
+
Compact.verify_contract('divider', divider){|divider| assert_equal 2, divider.divide(5,2) }
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
```
|
196
|
+
|
197
|
+
Running this (`rake examples`) produces the following report:
|
198
|
+
|
199
|
+
```
|
200
|
+
The following contracts could not be verified:
|
201
|
+
Role Name: subtracter
|
202
|
+
The following methods were invoked on test doubles without corresponding contract tests:
|
203
|
+
================================================================================
|
204
|
+
method: subtract
|
205
|
+
invoke with: [7, 2]
|
206
|
+
returns: 5
|
207
|
+
================================================================================
|
208
|
+
Role Name: multiplier
|
209
|
+
No test doubles mirror the following verified invocations:
|
210
|
+
================================================================================
|
211
|
+
method: multiply
|
212
|
+
invoke with: [2, 3]
|
213
|
+
returns: 6
|
214
|
+
================================================================================
|
215
|
+
Role Name: divider
|
216
|
+
Attempts to verify the following method invocations failed:
|
217
|
+
================================================================================
|
218
|
+
method: divide
|
219
|
+
invoke with: [5, 2]
|
220
|
+
expected: 2.5
|
221
|
+
Matching invocations returned the following values: [2]
|
222
|
+
================================================================================
|
223
|
+
```
|
224
|
+
|
225
|
+
## Status
|
226
|
+
My goals in sharing this publicly at this early stage are:
|
227
|
+
|
228
|
+
* To gauge interest in the idea
|
229
|
+
* To get feedback on the API
|
230
|
+
* To solicit anyone's input on some areas for further development below.
|
231
|
+
|
232
|
+
In version 0.1.0, "Comprehensive" should be understood as an aspiration rather than
|
233
|
+
a promise. In particular, I've only written a reporter for Minitest. RSpec is next on the agenda.
|
234
|
+
Tests have however been written against doubles created using Minitest::Mock, Mocha and `Object.new`.
|
235
|
+
|
236
|
+
And at the risk of stating the obvious, please do not rely on (version 0.1.0 of) this library to prove the basic correctness
|
237
|
+
of your safety-critical nuclear-powered aerospace software.
|
238
|
+
|
239
|
+
Of more pressing concern are some conceptual questions.
|
240
|
+
|
241
|
+
#### Mocks vs stubs
|
242
|
+
At present the design is clearly skewed towards verifying stubs - i.e. test doubles whose purpose is to
|
243
|
+
return a canned value. Matching on return values is a key part of the current verification. We use mocks
|
244
|
+
instead if we want to assert that some method is called for its side effects. In Java we would likely be
|
245
|
+
able to rely on both mocked and real methods having `void` return signature, but in Ruby we have implicit
|
246
|
+
returns and all bets are off. This probably motivates the introduction of some less stringent addition to
|
247
|
+
the Compact API that fulfills criteria similar to a verifying double.
|
248
|
+
|
249
|
+
#### Dependency Injection
|
250
|
+
This design relies on being able to create an instance of a test double and inject it as a dependency to the subject
|
251
|
+
of your collaboration test. Some more sophisticated mocking libraries offer ways in which you can use mocks
|
252
|
+
in ways that do not meet this requirement (such as mocking static factory methods). We can't help with that.
|
253
|
+
|
254
|
+
#### Class methods
|
255
|
+
This really generalises the above point. The current API depends crucially on being able to decorate instances
|
256
|
+
of your test double. I've tried implementations that redefine methods, and they work on a simple `Object.new` stub,
|
257
|
+
but instantly explode when you call `test_double.method` on a Minitest mock that isn't expecting to receive `:method`.
|
258
|
+
(Minitest::Mock doesn't actually support mocking class methods anyway as far as I am aware.) But it seems like any
|
259
|
+
solution to this problem will necessarily require a compromise on the goal of being agnostic about your other tools.
|
260
|
+
|
261
|
+
#### Value Objects in method invocations
|
262
|
+
The examples above all use integers as the method arguments and return values. When verifying contracts method
|
263
|
+
arguments and return values are compared using `==`. It's not clear to me how this approach can be generalised
|
264
|
+
to include parameters that are not value objects.
|
265
|
+
|
266
|
+
## Installation
|
267
|
+
I mean ... it's a gem.
|
268
|
+
|
269
|
+
Add this line to your application's Gemfile:
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
gem 'compact'
|
273
|
+
```
|
274
|
+
|
275
|
+
And then execute:
|
276
|
+
|
277
|
+
$ bundle
|
278
|
+
|
279
|
+
Or install it yourself as:
|
280
|
+
|
281
|
+
$ gem install compact
|
282
|
+
|
283
|
+
|
284
|
+
## Development
|
285
|
+
|
286
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
287
|
+
|
288
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
289
|
+
|
290
|
+
## Contributing
|
291
|
+
|
292
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/robwold/compact. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
293
|
+
|
294
|
+
## License
|
295
|
+
|
296
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
297
|
+
|
298
|
+
## Code of Conduct
|
299
|
+
|
300
|
+
Everyone interacting in the Compact project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the
|
301
|
+
[code of conduct](https://github.com/robwold/compact/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
require "rdoc/task"
|
4
|
+
|
5
|
+
Rake::TestTask.new(:test) do |t|
|
6
|
+
t.libs << "test"
|
7
|
+
t.libs << "lib"
|
8
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
9
|
+
end
|
10
|
+
|
11
|
+
Rake::TestTask.new(:examples) do |t|
|
12
|
+
t.libs << "examples"
|
13
|
+
#t.libs << "lib" # uncomment for local development without the gem installed.
|
14
|
+
t.test_files = FileList["examples/test/*_test.rb"]
|
15
|
+
end
|
16
|
+
|
17
|
+
Rake::RDocTask.new do |rdoc|
|
18
|
+
files = ['README.md', 'LICENSE.txt', 'lib/']
|
19
|
+
rdoc.rdoc_files.add(files)
|
20
|
+
rdoc.rdoc_dir = "doc"
|
21
|
+
rdoc.main = "README.md" # page to start on
|
22
|
+
end
|
23
|
+
|
24
|
+
task :default => :test
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "compact"
|
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
data/compact.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "compact/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "compact"
|
7
|
+
spec.version = Compact::VERSION
|
8
|
+
spec.authors = ["Rob"]
|
9
|
+
spec.email = ["rold9888@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = %q{Comprehensive Automated Contract Testing.}
|
12
|
+
spec.description = %q{Aims to help you keep the behaviour of your test doubles in line with that of your real dependencies.}
|
13
|
+
spec.homepage = "https://github.com/robwold/compact"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
#spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
17
|
+
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
19
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
20
|
+
#spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
25
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|examples)/}) }
|
26
|
+
end
|
27
|
+
spec.bindir = "exe"
|
28
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
29
|
+
spec.require_paths = ["lib"]
|
30
|
+
|
31
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
32
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
33
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
34
|
+
spec.add_development_dependency "simplecov", "~>0.18"
|
35
|
+
spec.add_development_dependency "mocha", "~>1.11"
|
36
|
+
end
|
data/lib/compact.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require "compact/version"
|
2
|
+
require 'compact/contract'
|
3
|
+
require 'compact/ledger'
|
4
|
+
require 'compact/verification_codes'
|
5
|
+
|
6
|
+
#
|
7
|
+
# This TOP-level module defines the entire public API of the gem.
|
8
|
+
#
|
9
|
+
module Compact
|
10
|
+
@@ledger = Ledger.new
|
11
|
+
|
12
|
+
# To record the interactions of your test_double,
|
13
|
+
# prepare inside a block passed to this method.
|
14
|
+
# Give the role played by the mock a name so we can
|
15
|
+
# cross-reference it with tests against the real
|
16
|
+
# implementation.
|
17
|
+
#
|
18
|
+
# my_watched_mock = Compact.prepare_double('role_name') do
|
19
|
+
# mock = MyMock.new
|
20
|
+
# mock.expect(:method_name, return_args, when_called_with)
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# The returned mock is decorated with an +ArgumentInterceptor+ that records:
|
24
|
+
# - methods sent to it
|
25
|
+
# - the arguments with which they were called
|
26
|
+
# - and the return values
|
27
|
+
# and stores these +Invocation+s in an instance of the +Ledger+ class for comparison
|
28
|
+
# with the corresponding contract tests in +verify_contract+.
|
29
|
+
def self.prepare_double(name, block = Proc.new)
|
30
|
+
@@ledger.prepare_double(name, block)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Calling this method checks that the +collaborator+ param is
|
34
|
+
# an object capable of fulfilling the role defined by +name+
|
35
|
+
# (for which see +prepare_double+).
|
36
|
+
#
|
37
|
+
# Example usage:
|
38
|
+
#
|
39
|
+
# Compact.verify_contract('role_name', myObject) do
|
40
|
+
# expected = return_value
|
41
|
+
# actual = myObject.method_name(*args_specified_by_test_double)
|
42
|
+
# assert_equal expected, actual
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
def self.verify_contract(name, collaborator, block = Proc.new )
|
46
|
+
@@ledger.verify_contract(name, collaborator, block)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Unlikely to be used by end users of this gem.
|
50
|
+
# Used to write test reporters that give us the low-down at the end of our suite.
|
51
|
+
def self.summary
|
52
|
+
@@ledger.summary
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Compact
|
2
|
+
class ArgumentInterceptor < SimpleDelegator
|
3
|
+
attr_accessor :invocations
|
4
|
+
|
5
|
+
def initialize(delegate)
|
6
|
+
@invocations = []
|
7
|
+
@contract = nil
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def register(contract)
|
12
|
+
@contract = contract
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(method, *args, &block)
|
16
|
+
returns = super
|
17
|
+
invocation = Invocation.new(method: method, args: args, returns: returns)
|
18
|
+
@invocations.push(invocation)
|
19
|
+
@contract.add_invocation(invocation) if @contract
|
20
|
+
returns
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require_relative './argument_interceptor'
|
2
|
+
require_relative './invocation'
|
3
|
+
require 'set'
|
4
|
+
module Compact
|
5
|
+
class Contract
|
6
|
+
attr_reader :specs
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@collaborator_invocations = Set.new
|
10
|
+
@test_double_invocations = Set.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def prepare_double(block = Proc.new)
|
14
|
+
double = block.call
|
15
|
+
interceptor = ArgumentInterceptor.new(double)
|
16
|
+
interceptor.register(self)
|
17
|
+
interceptor
|
18
|
+
end
|
19
|
+
|
20
|
+
def verified?
|
21
|
+
@test_double_invocations == @collaborator_invocations
|
22
|
+
end
|
23
|
+
|
24
|
+
def has_pending?
|
25
|
+
!pending_invocations.empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
def has_untested?
|
29
|
+
!untested_invocations.empty?
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_failing?
|
33
|
+
!failing_invocations.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def describe_untested_specs
|
37
|
+
headline = "The following methods were invoked on test doubles without corresponding contract tests:"
|
38
|
+
messages = untested_invocations.map(&:describe)
|
39
|
+
print_banner_separated(headline, messages)
|
40
|
+
end
|
41
|
+
|
42
|
+
def describe_pending_specs
|
43
|
+
headline = "No test doubles mirror the following verified invocations:"
|
44
|
+
messages = pending_invocations.map(&:describe)
|
45
|
+
print_banner_separated(headline, messages)
|
46
|
+
end
|
47
|
+
|
48
|
+
def describe_failing_specs
|
49
|
+
headline = "Attempts to verify the following method invocations failed:"
|
50
|
+
messages = failing_invocations.map do |invocation|
|
51
|
+
bad_results = unspecified_invocations.select{|p| p.matches_call(invocation) }
|
52
|
+
invocation.describe.gsub("returns", "expected") +
|
53
|
+
"\nMatching invocations returned the following values: #{bad_results.map(&:returns).inspect}"
|
54
|
+
end
|
55
|
+
print_banner_separated(headline, messages)
|
56
|
+
end
|
57
|
+
|
58
|
+
def verify(collaborator, block = Proc.new)
|
59
|
+
interceptor = ArgumentInterceptor.new(collaborator)
|
60
|
+
block.call(interceptor)
|
61
|
+
interceptor.invocations.each{|inv| @collaborator_invocations.add(inv) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_invocation(invocation)
|
65
|
+
@test_double_invocations.add(invocation)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def untested_invocations
|
71
|
+
uncorroborated_invocations - failing_invocations
|
72
|
+
end
|
73
|
+
|
74
|
+
def pending_invocations
|
75
|
+
unspecified_invocations.reject do |inv|
|
76
|
+
failing_invocations.any? {|failure| inv.matches_call(failure)}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def uncorroborated_invocations
|
81
|
+
(@test_double_invocations - @collaborator_invocations).to_a
|
82
|
+
end
|
83
|
+
|
84
|
+
def unspecified_invocations
|
85
|
+
(@collaborator_invocations - @test_double_invocations).to_a
|
86
|
+
end
|
87
|
+
|
88
|
+
def failing_invocations
|
89
|
+
uncorroborated_invocations.select do |spec|
|
90
|
+
unspecified_invocations.any?{|inv| inv.matches_call(spec)}
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def print_banner_separated(headline, messages)
|
95
|
+
banner = "================================================================================"
|
96
|
+
<<~MSG
|
97
|
+
#{headline}
|
98
|
+
#{banner}
|
99
|
+
#{messages.join(banner).strip}
|
100
|
+
#{banner}
|
101
|
+
MSG
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Compact
|
2
|
+
class Invocation
|
3
|
+
attr_reader :method, :args, :returns
|
4
|
+
def initialize( method:, args:, returns:)
|
5
|
+
@method = method
|
6
|
+
@args = args
|
7
|
+
@returns = returns
|
8
|
+
end
|
9
|
+
|
10
|
+
def == other_invocation
|
11
|
+
same_returns = returns == other_invocation.returns
|
12
|
+
matches_call(other_invocation) && same_returns
|
13
|
+
end
|
14
|
+
|
15
|
+
def eql? other_invocation
|
16
|
+
self.== other_invocation
|
17
|
+
end
|
18
|
+
|
19
|
+
def hash
|
20
|
+
describe.hash
|
21
|
+
end
|
22
|
+
|
23
|
+
def matches_call(other_invocation)
|
24
|
+
same_args = args == other_invocation.args
|
25
|
+
same_method = method == other_invocation.method
|
26
|
+
same_args && same_method
|
27
|
+
end
|
28
|
+
|
29
|
+
def describe
|
30
|
+
<<~DESCRIPTION
|
31
|
+
method: #{method}
|
32
|
+
invoke with: #{args.inspect}
|
33
|
+
returns: #{returns}
|
34
|
+
DESCRIPTION
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Compact
|
2
|
+
class Ledger
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@contracts = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def prepare_double(name, block = Proc.new)
|
9
|
+
@contracts[name] ||= Contract.new
|
10
|
+
contract = @contracts[name]
|
11
|
+
contract.prepare_double(block)
|
12
|
+
end
|
13
|
+
|
14
|
+
# deprecate this?
|
15
|
+
def record_contract(name, test_double, methods_to_watch = [])
|
16
|
+
@contracts[name] ||= Contract.new
|
17
|
+
contract = @contracts[name]
|
18
|
+
contract.watch(test_double, methods_to_watch)
|
19
|
+
end
|
20
|
+
|
21
|
+
def verify_contract(name, collaborator, block = Proc.new )
|
22
|
+
@contracts[name] ||= Contract.new
|
23
|
+
contract = @contracts[name]
|
24
|
+
contract.verify(collaborator, block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def summary
|
28
|
+
unverified_contracts = []
|
29
|
+
@contracts.each do |name, contract|
|
30
|
+
unverified_contracts << contract unless contract.verified?
|
31
|
+
end
|
32
|
+
if unverified_contracts.empty?
|
33
|
+
'All test double contracts are satisfied.'
|
34
|
+
else
|
35
|
+
msg = <<~EOF
|
36
|
+
The following contracts could not be verified:
|
37
|
+
#{summarise_untested_contracts}
|
38
|
+
#{summarise_pending_contracts}
|
39
|
+
#{summarise_failing_contracts}
|
40
|
+
EOF
|
41
|
+
msg.gsub(/\n+/, "\n")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# If the metaprogramming gets clunky to work with here git can help you out with
|
48
|
+
# some explicit, repetitive definitions.
|
49
|
+
[:untested, :pending, :failing].each do |category|
|
50
|
+
method_name = "summarise_#{category}_contracts"
|
51
|
+
test_for_presence = "has_#{category}?"
|
52
|
+
describe_category_specs = "describe_#{category}_specs"
|
53
|
+
define_method(method_name) do
|
54
|
+
return nil unless @contracts.values.any?{|c| c.send(test_for_presence) }
|
55
|
+
summary = ""
|
56
|
+
@contracts.each do |name, contract|
|
57
|
+
summary += "Role Name: #{name}\n#{contract.send(describe_category_specs)}" if contract.send(test_for_presence)
|
58
|
+
end
|
59
|
+
summary.strip
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
metadata
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: compact
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rob
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-03-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '13.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '13.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: simplecov
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.18'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.18'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: mocha
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.11'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.11'
|
83
|
+
description: Aims to help you keep the behaviour of your test doubles in line with
|
84
|
+
that of your real dependencies.
|
85
|
+
email:
|
86
|
+
- rold9888@gmail.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- ".ruby-gemset"
|
93
|
+
- ".ruby-version"
|
94
|
+
- ".travis.yml"
|
95
|
+
- CODE_OF_CONDUCT.md
|
96
|
+
- Gemfile
|
97
|
+
- Gemfile.lock
|
98
|
+
- LICENSE.txt
|
99
|
+
- README.md
|
100
|
+
- Rakefile
|
101
|
+
- bin/console
|
102
|
+
- bin/setup
|
103
|
+
- compact.gemspec
|
104
|
+
- lib/compact.rb
|
105
|
+
- lib/compact/argument_interceptor.rb
|
106
|
+
- lib/compact/contract.rb
|
107
|
+
- lib/compact/invocation.rb
|
108
|
+
- lib/compact/ledger.rb
|
109
|
+
- lib/compact/verification_codes.rb
|
110
|
+
- lib/compact/version.rb
|
111
|
+
- lib/minitest/compact_plugin.rb
|
112
|
+
- lib/minitest/compact_reporter.rb
|
113
|
+
homepage: https://github.com/robwold/compact
|
114
|
+
licenses:
|
115
|
+
- MIT
|
116
|
+
metadata:
|
117
|
+
homepage_uri: https://github.com/robwold/compact
|
118
|
+
source_code_uri: https://github.com/robwold/compact
|
119
|
+
post_install_message:
|
120
|
+
rdoc_options: []
|
121
|
+
require_paths:
|
122
|
+
- lib
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
requirements: []
|
134
|
+
rubyforge_project:
|
135
|
+
rubygems_version: 2.6.14
|
136
|
+
signing_key:
|
137
|
+
specification_version: 4
|
138
|
+
summary: Comprehensive Automated Contract Testing.
|
139
|
+
test_files: []
|