compact 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []