test_tube 1.0.0.beta0 → 2.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 +4 -4
- data/README.md +146 -17
- data/lib/test_tube.rb +45 -16
- data/lib/test_tube/base.rb +28 -3
- data/lib/test_tube/{content.rb → invoker.rb} +11 -9
- data/lib/test_tube/{liquid.rb → passer.rb} +7 -5
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6aa09337dc5d35dd53da1f517ff92bced3b0d0c4f9c25eef1cd9287b63e14bbf
|
4
|
+
data.tar.gz: 36026bb1f5f3532e70ff51ebc25a1ddcf8c6098214c438e6a6fb6ad0305b249c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 668493142591c985ece8e150691d7e60d6834b5da976361ff4c4aea34204b0c645aea61352424daf6964366014df424f881fd6b27fb416c581399395b9c7f8af
|
7
|
+
data.tar.gz: ea4ce58827e8643b54811082bee5861502d339daa4489ac69355fc06c172ca15f6c1129c6db2bbee171184d06211dba70cca56995302e18d176b3801e61e9f8f
|
data/README.md
CHANGED
@@ -1,11 +1,15 @@
|
|
1
1
|
# Test Tube
|
2
2
|
|
3
|
-
[](https://github.com/fixrb/test_tube/releases)
|
4
|
+
[](https://rubydoc.info/github/fixrb/test_tube/main)
|
5
|
+
[](https://github.com/fixrb/test_tube/actions?query=workflow%3Aci+branch%3Amain)
|
6
|
+
[](https://github.com/fixrb/test_tube/actions?query=workflow%3Arubocop+branch%3Amain)
|
7
|
+
[](https://github.com/fixrb/test_tube/raw/main/LICENSE.md)
|
6
8
|
|
7
9
|
> A test tube to conduct software experiments 🧪
|
8
10
|
|
11
|
+

|
12
|
+
|
9
13
|
## Installation
|
10
14
|
|
11
15
|
Add this line to your application's Gemfile:
|
@@ -28,8 +32,14 @@ gem install test_tube
|
|
28
32
|
|
29
33
|
## Usage
|
30
34
|
|
31
|
-
|
32
|
-
|
35
|
+
To make __TestTube__ available:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
require "test_tube"
|
39
|
+
```
|
40
|
+
|
41
|
+
Assuming we'd like to experiment on the answer to the Ultimate Question of Life,
|
42
|
+
the Universe, and Everything with the following _matcher_:
|
33
43
|
|
34
44
|
```ruby
|
35
45
|
class BeTheAnswer
|
@@ -37,23 +47,142 @@ class BeTheAnswer
|
|
37
47
|
42.equal?(yield)
|
38
48
|
end
|
39
49
|
end
|
50
|
+
```
|
51
|
+
|
52
|
+
A _matcher_ is an object that responds to the `matches?` method with a block
|
53
|
+
parameter representing the _actual value_ to be compared.
|
54
|
+
|
55
|
+
Back to our Ruby experiments, one possibility would be to `invoke` a whole block
|
56
|
+
of code:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
block_of_code = -> { "101010".to_i(2) }
|
60
|
+
|
61
|
+
experiment = TestTube.invoke(isolate: false, matcher: BeTheAnswer.new, negate: false, &block_of_code)
|
62
|
+
# => <TestTube actual=42 error=nil got=true>
|
63
|
+
|
64
|
+
experiment.actual # => 42
|
65
|
+
experiment.error # => nil
|
66
|
+
experiment.got # => true
|
67
|
+
```
|
68
|
+
|
69
|
+
An alternative would be to `pass` directly the actual value as a parameter:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
actual_value = "101010".to_i(2)
|
73
|
+
|
74
|
+
experiment = TestTube.pass(actual_value, matcher: BeTheAnswer.new, negate: false)
|
75
|
+
# => <TestTube actual=42 error=nil got=true>
|
76
|
+
|
77
|
+
experiment.actual # => 42
|
78
|
+
experiment.error # => nil
|
79
|
+
experiment.got # => true
|
80
|
+
```
|
81
|
+
|
82
|
+
### __Matchi__ matchers
|
83
|
+
|
84
|
+
To facilitate the addition of matchers, a collection is available via the
|
85
|
+
[__Matchi__ project](https://github.com/fixrb/matchi/).
|
86
|
+
|
87
|
+
Let's use a built-in __Matchi__ matcher:
|
88
|
+
|
89
|
+
```sh
|
90
|
+
gem install matchi
|
91
|
+
```
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
require "matchi"
|
95
|
+
```
|
96
|
+
|
97
|
+
An example of successful experience:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
experiment = TestTube.invoke(
|
101
|
+
isolate: false,
|
102
|
+
matcher: Matchi::Matcher::RaiseException.new(NoMethodError),
|
103
|
+
negate: false
|
104
|
+
) { "foo".blank? }
|
105
|
+
# => <TestTube actual=#<NoMethodError: undefined method `blank?' for "foo":String> error=nil got=true>
|
106
|
+
|
107
|
+
experiment.actual # => #<NoMethodError: undefined method `blank?' for "foo":String>
|
108
|
+
experiment.error # => nil
|
109
|
+
experiment.got # => true
|
110
|
+
```
|
111
|
+
|
112
|
+
Another example of an experiment that fails:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
experiment = TestTube.invoke(
|
116
|
+
isolate: false,
|
117
|
+
matcher: Matchi::Matcher::Equal.new(0.3),
|
118
|
+
negate: false,
|
119
|
+
&-> { 0.1 + 0.2 }
|
120
|
+
) # => <TestTube actual=0.30000000000000004 error=nil got=false>
|
121
|
+
|
122
|
+
experiment.actual # => 0.30000000000000004
|
123
|
+
experiment.error # => nil
|
124
|
+
experiment.got # => false
|
125
|
+
```
|
126
|
+
|
127
|
+
Finally, an experiment which causes an error:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
experiment = TestTube.invoke(
|
131
|
+
isolate: false,
|
132
|
+
matcher: Matchi::Matcher::Match.new(/^foo$/),
|
133
|
+
negate: false
|
134
|
+
) { BOOM }
|
135
|
+
# => <TestTube actual=nil error=#<NameError: uninitialized constant BOOM> got=nil>
|
136
|
+
|
137
|
+
experiment.actual # => nil
|
138
|
+
experiment.error # => #<NameError: uninitialized constant BOOM>
|
139
|
+
experiment.got # => nil
|
140
|
+
```
|
141
|
+
|
142
|
+
### Code isolation
|
143
|
+
|
144
|
+
When experimenting tests, side-effects may occur. Because they may or may not be
|
145
|
+
desired, an `isolate` option is available.
|
146
|
+
|
147
|
+
Let's for instance consider this block of code:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
greeting = "Hello, world!"
|
151
|
+
block_of_code = -> { greeting.gsub!("world", "Alice") } # => #<Proc:0x00007f87f71b9690 (irb):42 (lambda)>
|
152
|
+
```
|
40
153
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
154
|
+
By setting the `isolate` option to `true`, we can experiment while avoiding
|
155
|
+
side effects:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
experiment = TestTube.invoke(
|
159
|
+
isolate: true,
|
160
|
+
matcher: Matchi::Matcher::Eql.new("Hello, Alice!"),
|
161
|
+
negate: false,
|
162
|
+
&block_of_code
|
163
|
+
) # => <TestTube actual="Hello, Alice!" error=nil got=true>
|
164
|
+
|
165
|
+
greeting # => "Hello, world!"
|
166
|
+
```
|
167
|
+
|
168
|
+
Otherwise, we can experiment without any code isolation:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
experiment = TestTube.invoke(
|
172
|
+
isolate: false,
|
173
|
+
matcher: Matchi::Matcher::Eql.new("Hello, Alice!"),
|
174
|
+
negate: false,
|
175
|
+
&block_of_code
|
176
|
+
) # => <TestTube actual="Hello, Alice!" error=nil got=true>
|
177
|
+
|
178
|
+
greeting # => "Hello, Alice!"
|
52
179
|
```
|
53
180
|
|
54
181
|
## Contact
|
55
182
|
|
56
183
|
* Source code: https://github.com/fixrb/test_tube
|
184
|
+
* Chinese blog post: https://ruby-china.org/topics/41390
|
185
|
+
* Japanese blog post: https://qiita.com/cyril/items/36174b619ff1852c80ec
|
57
186
|
|
58
187
|
## Versioning
|
59
188
|
|
@@ -61,7 +190,7 @@ __Test Tube__ follows [Semantic Versioning 2.0](https://semver.org/).
|
|
61
190
|
|
62
191
|
## License
|
63
192
|
|
64
|
-
The [gem](https://rubygems.org/gems/test_tube) is available as open source under the terms of the [MIT License](https://
|
193
|
+
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).
|
65
194
|
|
66
195
|
***
|
67
196
|
|
data/lib/test_tube.rb
CHANGED
@@ -1,26 +1,55 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative File.join("test_tube", "
|
4
|
-
require_relative File.join("test_tube", "
|
3
|
+
require_relative File.join("test_tube", "invoker")
|
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
|
9
|
-
# @param
|
10
|
-
# @param
|
11
|
-
# @param
|
12
|
-
#
|
13
|
-
# @
|
14
|
-
|
15
|
-
|
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.
|
14
|
+
#
|
15
|
+
# @example
|
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
|
27
|
+
#
|
28
|
+
# @return [Invoker] A software experiment.
|
29
|
+
def self.invoke(isolate:, matcher:, negate:, &input)
|
30
|
+
Invoker.new(isolate: isolate, matcher: matcher, negate: negate, &input)
|
16
31
|
end
|
17
32
|
|
18
|
-
# @param input
|
19
|
-
# @param matcher
|
20
|
-
# @param negate
|
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.
|
36
|
+
#
|
37
|
+
# @example
|
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
|
49
|
+
# )
|
21
50
|
#
|
22
|
-
# @return [
|
23
|
-
def self.
|
24
|
-
|
51
|
+
# @return [Passer] A software experiment.
|
52
|
+
def self.pass(input, matcher:, negate:)
|
53
|
+
Passer.new(input, matcher: matcher, negate: negate)
|
25
54
|
end
|
26
55
|
end
|
data/lib/test_tube/base.rb
CHANGED
@@ -1,15 +1,40 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module TestTube
|
4
|
-
#
|
4
|
+
# Abstract class representing the state of an experiment.
|
5
|
+
#
|
6
|
+
# @api private
|
5
7
|
class Base
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
9
|
-
|
10
|
-
|
8
|
+
# Evaluate an actual value invoking it with #call method.
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class Invoker < Base
|
12
|
+
# Class initializer.
|
11
13
|
#
|
12
14
|
# rubocop:disable Lint/RescueException, Metrics/MethodLength
|
13
15
|
#
|
14
|
-
# @param
|
15
|
-
# @param
|
16
|
-
# @param
|
17
|
-
# @param
|
18
|
-
def initialize(
|
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
|
24
|
+
value = if isolate
|
23
25
|
send_call.to!(input)
|
24
26
|
else
|
25
27
|
send_call.to(input)
|
@@ -3,18 +3,20 @@
|
|
3
3
|
require_relative "base"
|
4
4
|
|
5
5
|
module TestTube
|
6
|
-
#
|
7
|
-
|
8
|
-
|
6
|
+
# Evaluate an actual value passed in parameter.
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
class Passer < Base
|
10
|
+
# Class initializer.
|
9
11
|
#
|
10
|
-
# @param input [#object_id]
|
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:)
|
14
16
|
super()
|
15
17
|
|
16
18
|
@actual = input
|
17
|
-
@got = negate ^ matcher.matches? {
|
19
|
+
@got = negate ^ matcher.matches? { input }
|
18
20
|
end
|
19
21
|
end
|
20
22
|
end
|
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
|
4
|
+
version: 2.1.0
|
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-
|
11
|
+
date: 2021-07-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: defi
|
@@ -160,8 +160,8 @@ files:
|
|
160
160
|
- README.md
|
161
161
|
- lib/test_tube.rb
|
162
162
|
- lib/test_tube/base.rb
|
163
|
-
- lib/test_tube/
|
164
|
-
- lib/test_tube/
|
163
|
+
- lib/test_tube/invoker.rb
|
164
|
+
- lib/test_tube/passer.rb
|
165
165
|
homepage: https://github.com/fixrb/test_tube
|
166
166
|
licenses:
|
167
167
|
- MIT
|
@@ -177,9 +177,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
177
177
|
version: 2.7.0
|
178
178
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
179
179
|
requirements:
|
180
|
-
- - "
|
180
|
+
- - ">="
|
181
181
|
- !ruby/object:Gem::Version
|
182
|
-
version:
|
182
|
+
version: '0'
|
183
183
|
requirements: []
|
184
184
|
rubygems_version: 3.1.6
|
185
185
|
signing_key:
|