test_tube 1.0.0 → 2.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed96729c5b8c3a0ccef89cc6739da3e46e54c70eaf3e0975ca7af8d74db23d26
4
- data.tar.gz: b534c7cb0d0e5de7ce02c328b1be76960214ec4e6a4d45b031fafb1d9ffac136
3
+ metadata.gz: 1cac8bf6e09699d7c35d379b70b15cec448fce162d0664eef890405483c7a305
4
+ data.tar.gz: 34d205b4ba02fcc13fecd4478d2e47ca1409e74863abd99749de44332e8c69f3
5
5
  SHA512:
6
- metadata.gz: 48ac22dc6ada96e416df896a7c6e4bf7242e2391ce3788c28b2a2aeaa2ee93a18ed5f690be51dc0a434c97a8185c112a796cb7f0985dabe90eb39b33f8b4fee9
7
- data.tar.gz: 5a508032716cd4836009b9ac4b2b1fcdfa07e7b579fbfae771563bdebb81cf6e68efa2a7edbe897f8162015c8e45c0ba46346785026867f227cae14da4c398ca
6
+ metadata.gz: 29bbf3f347a546b393f5c180c94d74d41642501610abac8b4dda30ddfeca51606b7cece16a7121d3dd30331789b169039bfee52693056c09122d2fd51021821c
7
+ data.tar.gz: f13e1130f39b17577b066e7fea60e0bd4935a37eba7c27079bc5835e1dc8afbf87ef4b985f1baa4007d1b8a069ecd8e78607fef5a2a1abdbc134c633bbfaba4c
data/README.md CHANGED
@@ -1,11 +1,15 @@
1
1
  # Test Tube
2
2
 
3
- [![Build Status](https://api.travis-ci.org/fixrb/test_tube.svg?branch=main)](https://travis-ci.org/fixrb/test_tube)
4
- [![Gem Version](https://badge.fury.io/rb/test_tube.svg)](https://rubygems.org/gems/test_tube)
5
- [![Documentation](https://img.shields.io/:yard-docs-38c800.svg)](https://rubydoc.info/gems/test_tube)
3
+ [![Version](https://img.shields.io/github/v/tag/fixrb/test_tube?label=Version&logo=github)](https://github.com/fixrb/test_tube/releases)
4
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/fixrb/test_tube/main)
5
+ [![CI](https://github.com/fixrb/test_tube/workflows/CI/badge.svg?branch=main)](https://github.com/fixrb/test_tube/actions?query=workflow%3Aci+branch%3Amain)
6
+ [![RuboCop](https://github.com/fixrb/test_tube/workflows/RuboCop/badge.svg?branch=main)](https://github.com/fixrb/test_tube/actions?query=workflow%3Arubocop+branch%3Amain)
7
+ [![License](https://img.shields.io/github/license/fixrb/test_tube?label=License&logo=github)](https://github.com/fixrb/test_tube/raw/main/LICENSE.md)
6
8
 
7
9
  > A test tube to conduct software experiments 🧪
8
10
 
11
+ ![A researcher experimenting with Ruby code](https://github.com/fixrb/test_tube/raw/main/img/social-media-preview.png)
12
+
9
13
  ## Installation
10
14
 
11
15
  Add this line to your application's Gemfile:
@@ -28,6 +32,12 @@ gem install test_tube
28
32
 
29
33
  ## Usage
30
34
 
35
+ To make __TestTube__ available:
36
+
37
+ ```ruby
38
+ require "test_tube"
39
+ ```
40
+
31
41
  Assuming we'd like to experiment on the answer to the Ultimate Question of Life,
32
42
  the Universe, and Everything with the following matcher:
33
43
 
@@ -39,40 +49,136 @@ class BeTheAnswer
39
49
  end
40
50
  ```
41
51
 
42
- One possibility would be to challenge a whole block of code:
52
+ One possibility would be to `invoke` a whole block of code:
53
+
54
+ ```ruby
55
+ block_of_code = -> { "101010".to_i(2) }
56
+
57
+ experiment = TestTube.invoke(isolate: false, matcher: BeTheAnswer.new, negate: false, &block_of_code)
58
+ # => <TestTube actual=42 error=nil got=true>
59
+
60
+ experiment.actual # => 42
61
+ experiment.error # => nil
62
+ experiment.got # => true
63
+ ```
64
+
65
+ An alternative would be to `pass` directly the actual value as a parameter:
66
+
67
+ ```ruby
68
+ actual_value = "101010".to_i(2)
69
+
70
+ experiment = TestTube.pass(actual_value, matcher: BeTheAnswer.new, negate: false)
71
+ # => <TestTube actual=42 error=nil got=true>
72
+
73
+ experiment.actual # => 42
74
+ experiment.error # => nil
75
+ experiment.got # => true
76
+ ```
77
+
78
+ ### __Matchi__ matchers
79
+
80
+ To facilitate the addition of matchers, a collection is available via the
81
+ [__Matchi__ project](https://github.com/fixrb/matchi/).
82
+
83
+ Let's use a built-in __Matchi__ matcher:
84
+
85
+ ```sh
86
+ gem install matchi
87
+ ```
88
+
89
+ ```ruby
90
+ require "matchi"
91
+ ```
92
+
93
+ An example of successful experience:
94
+
95
+ ```ruby
96
+ experiment = TestTube.invoke(
97
+ isolate: false,
98
+ matcher: Matchi::RaiseException.new(:NoMethodError),
99
+ negate: false
100
+ ) { "foo".blank? }
101
+ # => <TestTube actual=#<NoMethodError: undefined method `blank?' for "foo":String> error=nil got=true>
102
+
103
+ experiment.actual # => #<NoMethodError: undefined method `blank?' for "foo":String>
104
+ experiment.error # => nil
105
+ experiment.got # => true
106
+ ```
107
+
108
+ Another example of an experiment that fails:
43
109
 
44
110
  ```ruby
45
- tt = TestTube.invoke(
46
- -> { "101010".to_i(2) },
47
- isolation: false,
48
- matcher: BeTheAnswer.new,
49
- negate: false
50
- )
51
- # => #<TestTube::Content:0x00007fb3b328b248 @actual=42, @got=true, @error=nil>
52
-
53
- tt.actual # => 42
54
- tt.error # => nil
55
- tt.got # => true
111
+ experiment = TestTube.invoke(
112
+ isolate: false,
113
+ matcher: Matchi::Be.new(0.3),
114
+ negate: false,
115
+ &-> { 0.1 + 0.2 }
116
+ ) # => <TestTube actual=0.30000000000000004 error=nil got=false>
117
+
118
+ experiment.actual # => 0.30000000000000004
119
+ experiment.error # => nil
120
+ experiment.got # => false
56
121
  ```
57
122
 
58
- An alternative would be to challenge a value passed as a parameter:
123
+ Finally, an experiment which causes an error:
59
124
 
60
125
  ```ruby
61
- tt = TestTube.pass(
62
- "101010".to_i(2),
63
- matcher: BeTheAnswer.new,
126
+ experiment = TestTube.invoke(
127
+ isolate: false,
128
+ matcher: Matchi::Match.new(/^foo$/),
64
129
  negate: false
65
- )
66
- # => #<TestTube::Passer:0x00007f85c229c2d8 @actual=42, @got=true>
130
+ ) { BOOM }
131
+ # => <TestTube actual=nil error=#<NameError: uninitialized constant BOOM> got=nil>
132
+
133
+ experiment.actual # => nil
134
+ experiment.error # => #<NameError: uninitialized constant BOOM>
135
+ experiment.got # => nil
136
+ ```
137
+
138
+ ### Code isolation
67
139
 
68
- tt.actual # => 42
69
- tt.error # => nil
70
- tt.got # => true
140
+ When experimenting tests, side-effects may occur. Because they may or may not be
141
+ desired, an `isolate` option is available.
142
+
143
+ Let's for instance consider this block of code:
144
+
145
+ ```ruby
146
+ greeting = "Hello, world!"
147
+ block_of_code = -> { greeting.gsub!("world", "Alice") } # => #<Proc:0x00007f87f71b9690 (irb):42 (lambda)>
148
+ ```
149
+
150
+ By setting the `isolate` option to `true`, we can experiment while avoiding
151
+ side effects:
152
+
153
+ ```ruby
154
+ experiment = TestTube.invoke(
155
+ isolate: true,
156
+ matcher: Matchi::Eq.new("Hello, Alice!"),
157
+ negate: false,
158
+ &block_of_code
159
+ ) # => <TestTube actual="Hello, Alice!" error=nil got=true>
160
+
161
+ greeting # => "Hello, world!"
162
+ ```
163
+
164
+ Otherwise, we can experiment without any code isolation:
165
+
166
+ ```ruby
167
+ experiment = TestTube.invoke(
168
+ isolate: false,
169
+ matcher: Matchi::Eq.new("Hello, Alice!"),
170
+ negate: false,
171
+ &block_of_code
172
+ ) # => <TestTube actual="Hello, Alice!" error=nil got=true>
173
+
174
+ greeting # => "Hello, Alice!"
71
175
  ```
72
176
 
73
177
  ## Contact
74
178
 
75
179
  * Source code: https://github.com/fixrb/test_tube
180
+ * Chinese blog post: https://ruby-china.org/topics/41390
181
+ * Japanese blog post: https://qiita.com/cyril/items/36174b619ff1852c80ec
76
182
 
77
183
  ## Versioning
78
184
 
@@ -80,7 +186,7 @@ __Test Tube__ follows [Semantic Versioning 2.0](https://semver.org/).
80
186
 
81
187
  ## License
82
188
 
83
- The [gem](https://rubygems.org/gems/test_tube) is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
189
+ The [gem](https://rubygems.org/gems/test_tube) is available as open source under the terms of the [MIT License](https://github.com/fixrb/test_tube/raw/main/LICENSE.md).
84
190
 
85
191
  ***
86
192
 
data/lib/test_tube.rb CHANGED
@@ -4,34 +4,48 @@ require_relative File.join("test_tube", "invoker")
4
4
  require_relative File.join("test_tube", "passer")
5
5
 
6
6
  # Namespace for the TestTube library.
7
+ #
8
+ # @api public
7
9
  module TestTube
8
- # @param input [#call] The callable object to test.
9
- # @param isolation [Boolean] Compute in isolation or not.
10
- # @param matcher [#matches?] A matcher.
11
- # @param negate [Boolean] Invert the matcher or not.
10
+ # @param isolate [Boolean] Compute in a subprocess.
11
+ # @param matcher [#matches?] A matcher.
12
+ # @param negate [Boolean] Invert the matcher or not.
13
+ # @param input [Proc] The callable object to test.
12
14
  #
13
15
  # @example
14
- # invoke(
15
- # -> { "101010".to_i(2) },
16
- # isolation: false,
17
- # matcher: BeTheAnswer.new,
18
- # negate: false
19
- # )
16
+ # require "test_tube"
17
+ #
18
+ # class BeTheAnswer
19
+ # def matches?
20
+ # 42.equal?(yield)
21
+ # end
22
+ # end
23
+ #
24
+ # TestTube.invoke(isolate: false, matcher: BeTheAnswer.new, negate: false) do
25
+ # "101010".to_i(2)
26
+ # end
20
27
  #
21
28
  # @return [Invoker] A software experiment.
22
- def self.invoke(input, isolation:, matcher:, negate:)
23
- Invoker.new(input, isolation: isolation, matcher: matcher, negate: negate)
29
+ def self.invoke(isolate:, matcher:, negate:, &input)
30
+ Invoker.new(isolate: isolate, matcher: matcher, negate: negate, &input)
24
31
  end
25
32
 
26
- # @param input [#object_id] The callable object to test.
27
- # @param matcher [#matches?] A matcher.
28
- # @param negate [Boolean] Invert the matcher or not.
33
+ # @param input [#object_id] The actual value to test.
34
+ # @param matcher [#matches?] A matcher.
35
+ # @param negate [Boolean] Invert the matcher or not.
29
36
  #
30
37
  # @example
31
- # pass(
32
- # "101010".to_i(2),
33
- # matcher: BeTheAnswer.new,
34
- # negate: false
38
+ # require "test_tube"
39
+ #
40
+ # class BeTheAnswer
41
+ # def matches?
42
+ # 42.equal?(yield)
43
+ # end
44
+ # end
45
+ #
46
+ # TestTube.pass("101010".to_i(2),
47
+ # matcher: BeTheAnswer.new,
48
+ # negate: false
35
49
  # )
36
50
  #
37
51
  # @return [Passer] A software experiment.
@@ -1,15 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TestTube
4
- # The base class.
5
- class Base
4
+ # Abstract class representing the state of an experiment.
5
+ #
6
+ # @api private
7
+ class Base < ::BasicObject
8
+ # Expectation's actual value.
9
+ #
6
10
  # @return [#object_id] The actual value.
11
+ #
12
+ # @api public
7
13
  attr_reader :actual
8
14
 
9
- # @return [Exception, nil] The raised exception.
15
+ # Expectation's raised error.
16
+ #
17
+ # @return [Exception, nil] The raised error.
18
+ #
19
+ # @api public
10
20
  attr_reader :error
11
21
 
12
- # @return [Boolean, nil] The test result.
22
+ # Expectation's returned boolean value.
23
+ #
24
+ # @return [Boolean, nil] The returned boolean value.
25
+ #
26
+ # @api public
13
27
  attr_reader :got
28
+
29
+ # A string containing a human-readable representation of the experiment.
30
+ #
31
+ # @return [String] The human-readable representation of the experiment.
32
+ #
33
+ # @api public
34
+ def inspect
35
+ "<TestTube actual=#{actual.inspect} error=#{error.inspect} got=#{got.inspect}>"
36
+ end
37
+
38
+ alias to_s inspect
14
39
  end
15
40
  end
@@ -5,21 +5,23 @@ require "defi"
5
5
  require_relative "base"
6
6
 
7
7
  module TestTube
8
- # The invoker class is great for blocks.
8
+ # Evaluate an actual value invoking it with #call method.
9
+ #
10
+ # @api private
9
11
  class Invoker < Base
10
- # Software experiments.
12
+ # Class initializer.
11
13
  #
12
14
  # rubocop:disable Lint/RescueException, Metrics/MethodLength
13
15
  #
14
- # @param input [#call] The callable object to test.
15
- # @param isolation [Boolean] Compute in isolation or not.
16
- # @param matcher [#matches?] A matcher.
17
- # @param negate [Boolean] Invert the matcher or not.
18
- def initialize(input, isolation:, matcher:, negate:)
16
+ # @param isolate [Boolean] Compute in a subprocess.
17
+ # @param matcher [#matches?] A matcher.
18
+ # @param negate [Boolean] Invert the matcher or not.
19
+ # @param input [Proc] The callable object to test.
20
+ def initialize(isolate:, matcher:, negate:, &input)
19
21
  super()
20
22
 
21
23
  @got = negate ^ matcher.matches? do
22
- value = if isolation
24
+ value = if isolate
23
25
  send_call.to!(input)
24
26
  else
25
27
  send_call.to(input)
@@ -3,11 +3,13 @@
3
3
  require_relative "base"
4
4
 
5
5
  module TestTube
6
- # The passer class is great for values.
6
+ # Evaluate an actual value passed in parameter.
7
+ #
8
+ # @api private
7
9
  class Passer < Base
8
- # Software experiments.
10
+ # Class initializer.
9
11
  #
10
- # @param input [#object_id] An actual value to test.
12
+ # @param input [#object_id] The actual value to test.
11
13
  # @param matcher [#matches?] A matcher.
12
14
  # @param negate [Boolean] Invert the matcher or not.
13
15
  def initialize(input, matcher:, negate:)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test_tube
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-19 00:00:00.000000000 Z
11
+ date: 2021-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: defi
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 2.0.5
19
+ version: 2.0.6
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 2.0.5
26
+ version: 2.0.6
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: brutal
29
29
  requirement: !ruby/object:Gem::Requirement