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 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
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
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
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.5.0
7
+ before_install: gem install bundler -v 2.0.2
@@ -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
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in compact.gemspec
4
+ gemspec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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
@@ -0,0 +1,6 @@
1
+ module Compact
2
+ PENDING = :pending
3
+ VERIFIED = :verified
4
+ FAILING = :failing
5
+ UNTESTED = :untested
6
+ end
@@ -0,0 +1,3 @@
1
+ module Compact
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,9 @@
1
+ require_relative './compact_reporter'
2
+ module Minitest
3
+ def self.plugin_compact_options(opts, options) ; end
4
+
5
+
6
+ def self.plugin_compact_init(options)
7
+ self.reporter << CompactReporter.new
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ require_relative '../compact/ledger'
2
+ module Minitest
3
+ class CompactReporter < AbstractReporter
4
+
5
+ def record(result); end
6
+
7
+ def report
8
+ puts Compact.summary
9
+ end
10
+ end
11
+ 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: []