test_tube 1.0.0 → 2.1.1

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