rspock 2.4.0 → 2.5.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/CHANGELOG.md +14 -1
- data/Gemfile.lock +1 -1
- data/README.md +41 -0
- data/lib/rspock/ast/interaction_to_mocha_mock_transformation.rb +4 -4
- data/lib/rspock/ast/node.rb +11 -3
- data/lib/rspock/ast/parser/expect_block.rb +5 -0
- data/lib/rspock/ast/parser/interaction_parser.rb +3 -3
- data/lib/rspock/ast/parser/statement_parser.rb +27 -0
- data/lib/rspock/ast/parser/then_block.rb +5 -0
- data/lib/rspock/ast/test_method_transformation.rb +37 -3
- data/lib/rspock/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d6c10b52d342827b092af647c5bc144f4e381dff91167eedf93a0f99c9488ee2
|
|
4
|
+
data.tar.gz: 20ea274b4fa0e3a79eefb88f33c53ddf84adfc582fd37fe3e2ee9688a8137747
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '019855311bf85f23d10fb75d3b1c0020b52fbf2ae7083205ca69a3f15b142ee7c2ba24e9be0506c690e65b42302b347ea7dc256c596edf1069ec8a0700aad666'
|
|
7
|
+
data.tar.gz: a559d70b0f3509d361234019935a2105c4a2b96ddac5ce0df7198edff346fe83a5c7a9c889d5152fbf0a856a60edd069017c3b24f88b144eda651ab35a57b47b
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.5.0] - 2026-02-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Exception conditions: `raises ExceptionClass` in Then blocks wraps the preceding When block in an exception assertion.
|
|
15
|
+
- Exception capture: `e = raises ExceptionClass` captures the exception for further assertions in the same Then block.
|
|
16
|
+
- Exception conditions work with data-driven `Where` blocks.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Renamed interaction outcome nodes from `rspock_returns` / `rspock_raises` to `rspock_stub_returns` / `rspock_stub_raises` to distinguish them from the new exception condition `rspock_raises`.
|
|
21
|
+
|
|
10
22
|
## [2.4.0] - 2026-02-28
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -140,7 +152,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
140
152
|
|
|
141
153
|
- Initial release.
|
|
142
154
|
|
|
143
|
-
[Unreleased]: https://github.com/rspockframework/rspock/compare/v2.
|
|
155
|
+
[Unreleased]: https://github.com/rspockframework/rspock/compare/v2.5.0...HEAD
|
|
156
|
+
[2.5.0]: https://github.com/rspockframework/rspock/compare/v2.4.0...v2.5.0
|
|
144
157
|
[2.4.0]: https://github.com/rspockframework/rspock/compare/v2.3.1...v2.4.0
|
|
145
158
|
[2.3.1]: https://github.com/rspockframework/rspock/compare/v2.3.0...v2.3.1
|
|
146
159
|
[2.3.0]: https://github.com/rspockframework/rspock/compare/v2.2.0...v2.3.0
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Note: RSpock is heavily inspired by Spock for the Groovy programming language.
|
|
|
19
19
|
* Spock-style implicit assertions: every statement in Then/Expect blocks is an assertion (no assertion API needed)
|
|
20
20
|
* Binary operator assertions: `==`, `!=`, `=~`, `!~`, `>`, `<`, `>=`, `<=` with clear error messages
|
|
21
21
|
* General statement assertions: bare boolean expressions (e.g. `obj.valid?`, `list.include?(x)`) and negation (`!obj.empty?`) with source-text error messages
|
|
22
|
+
* [Exception conditions](#exception-conditions): `raises ExceptionClass` in Then blocks wraps the When block in an exception assertion, with optional capture for property assertions
|
|
22
23
|
* [Interaction-based testing](#mocking-with-interactions), i.e. `1 * object.receive("message")` in Then blocks, with optional [return value stubbing](#stubbing-return-values) via `>>`, [exception stubbing](#stubbing-exceptions) via `>> raises(...)`, and [block forwarding verification](#block-forwarding-verification) via `&block`
|
|
23
24
|
* (Planned) BDD-style custom reporter that outputs information from Code Blocks
|
|
24
25
|
* (Planned) Capture all Then block violations
|
|
@@ -166,6 +167,46 @@ The Then block describes the response from the stimulus. Following Spock's core
|
|
|
166
167
|
|
|
167
168
|
**Variable assignments** pass through unchanged and execute in source order after the stimulus.
|
|
168
169
|
|
|
170
|
+
##### Exception Conditions
|
|
171
|
+
|
|
172
|
+
Use `raises` in a Then block to assert that the preceding When block raises a specific exception:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
When "Dividing by zero"
|
|
176
|
+
1 / 0
|
|
177
|
+
|
|
178
|
+
Then "An error is raised"
|
|
179
|
+
raises ZeroDivisionError
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
To inspect the exception, capture it into a variable:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
When "Parsing bad input"
|
|
186
|
+
JSON.parse("not json")
|
|
187
|
+
|
|
188
|
+
Then "A parse error is raised with a message"
|
|
189
|
+
e = raises JSON::ParserError
|
|
190
|
+
e.message.include?("unexpected token")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The captured variable is available for further assertions in the same Then block. Exception conditions work with data-driven `Where` blocks as well:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
When "Parsing invalid input"
|
|
197
|
+
JSON.parse(input)
|
|
198
|
+
|
|
199
|
+
Then "The expected error is raised"
|
|
200
|
+
raises expected_error
|
|
201
|
+
|
|
202
|
+
Where
|
|
203
|
+
input | expected_error
|
|
204
|
+
"not json" | JSON::ParserError
|
|
205
|
+
"{invalid" | JSON::ParserError
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Only **one** `raises` condition is allowed per Then block, and `raises` is **not supported** in Expect blocks (use a When + Then block instead).
|
|
209
|
+
|
|
169
210
|
#### Expect Block
|
|
170
211
|
|
|
171
212
|
The Expect block is useful when expressing the stimulus and the response in one statement is more natural. The same assertion rules apply as in Then blocks — every statement is an assertion unless it's a variable assignment.
|
|
@@ -10,14 +10,14 @@ module RSpock
|
|
|
10
10
|
# Output: receiver.expects(:message).with(*args).times(n).returns(value)
|
|
11
11
|
#
|
|
12
12
|
# The outcome node type maps directly to the Mocha chain method:
|
|
13
|
-
# :
|
|
14
|
-
# :
|
|
13
|
+
# :rspock_stub_returns -> .returns(value)
|
|
14
|
+
# :rspock_stub_raises -> .raises(exception_class, ...)
|
|
15
15
|
#
|
|
16
16
|
# When block_pass is present, wraps the expects chain with a BlockCapture.capture call.
|
|
17
17
|
class InteractionToMochaMockTransformation < ASTTransform::AbstractTransformation
|
|
18
18
|
OUTCOME_METHODS = {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
rspock_stub_returns: :returns,
|
|
20
|
+
rspock_stub_raises: :raises,
|
|
21
21
|
}.freeze
|
|
22
22
|
|
|
23
23
|
def initialize(index = 0)
|
data/lib/rspock/ast/node.rb
CHANGED
|
@@ -73,12 +73,20 @@ module RSpock
|
|
|
73
73
|
class OutcomeNode < Node
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
class
|
|
77
|
-
register :
|
|
76
|
+
class StubReturnsNode < OutcomeNode
|
|
77
|
+
register :rspock_stub_returns
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
class
|
|
80
|
+
class StubRaisesNode < OutcomeNode
|
|
81
|
+
register :rspock_stub_raises
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class RaisesNode < Node
|
|
81
85
|
register :rspock_raises
|
|
86
|
+
|
|
87
|
+
def exception_class = children[0]
|
|
88
|
+
def capture_var = children[1]
|
|
89
|
+
def capture_name = capture_var&.children&.[](0)
|
|
82
90
|
end
|
|
83
91
|
|
|
84
92
|
class InteractionNode < Node
|
|
@@ -25,6 +25,11 @@ module RSpock
|
|
|
25
25
|
def to_rspock_node
|
|
26
26
|
statement_parser = StatementParser.new
|
|
27
27
|
spock_children = @children.map { |child| statement_parser.parse(child) }
|
|
28
|
+
|
|
29
|
+
if spock_children.any? { |c| c.type == :rspock_raises }
|
|
30
|
+
raise BlockError, "raises() is not supported in Expect blocks @ #{range}. Use a When + Then block instead."
|
|
31
|
+
end
|
|
32
|
+
|
|
28
33
|
s(:rspock_expect, *spock_children)
|
|
29
34
|
end
|
|
30
35
|
end
|
|
@@ -14,7 +14,7 @@ module RSpock
|
|
|
14
14
|
# [1] receiver - e.g. s(:send, nil, :subscriber)
|
|
15
15
|
# [2] message - e.g. s(:sym, :receive)
|
|
16
16
|
# [3] args - nil if no args, s(:array, *arg_nodes) otherwise
|
|
17
|
-
# [4] outcome - nil if no >>, otherwise s(:
|
|
17
|
+
# [4] outcome - nil if no >>, otherwise s(:rspock_stub_returns, value) or s(:rspock_stub_raises, *args)
|
|
18
18
|
# [5] block_pass - nil if no &, otherwise s(:block_pass, ...)
|
|
19
19
|
class InteractionParser
|
|
20
20
|
include RSpock::AST::NodeBuilder
|
|
@@ -63,9 +63,9 @@ module RSpock
|
|
|
63
63
|
|
|
64
64
|
def parse_outcome(node)
|
|
65
65
|
if node.type == :send && node.children[0].nil? && node.children[1] == :raises
|
|
66
|
-
s(:
|
|
66
|
+
s(:rspock_stub_raises, *node.children[2..])
|
|
67
67
|
else
|
|
68
|
-
s(:
|
|
68
|
+
s(:rspock_stub_returns, node)
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
@@ -16,6 +16,7 @@ module RSpock
|
|
|
16
16
|
ASSIGNMENT_TYPES = %i[lvasgn masgn op_asgn or_asgn and_asgn].freeze
|
|
17
17
|
|
|
18
18
|
def parse(node)
|
|
19
|
+
return build_raises(node) if raises_condition?(node)
|
|
19
20
|
return node if assignment?(node)
|
|
20
21
|
return build_binary_statement(node) if binary_statement?(node)
|
|
21
22
|
|
|
@@ -24,6 +25,32 @@ module RSpock
|
|
|
24
25
|
|
|
25
26
|
private
|
|
26
27
|
|
|
28
|
+
def raises_condition?(node)
|
|
29
|
+
direct_raises?(node) || assigned_raises?(node)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def direct_raises?(node)
|
|
33
|
+
node.type == :send && node.children[0].nil? && node.children[1] == :raises
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def assigned_raises?(node)
|
|
37
|
+
node.type == :lvasgn &&
|
|
38
|
+
node.children[1]&.type == :send &&
|
|
39
|
+
node.children[1].children[0].nil? &&
|
|
40
|
+
node.children[1].children[1] == :raises
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build_raises(node)
|
|
44
|
+
if node.type == :lvasgn
|
|
45
|
+
variable = s(:sym, node.children[0])
|
|
46
|
+
exception_class = node.children[1].children[2]
|
|
47
|
+
s(:rspock_raises, exception_class, variable)
|
|
48
|
+
else
|
|
49
|
+
exception_class = node.children[2]
|
|
50
|
+
s(:rspock_raises, exception_class)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
27
54
|
def assignment?(node)
|
|
28
55
|
ASSIGNMENT_TYPES.include?(node.type)
|
|
29
56
|
end
|
|
@@ -30,6 +30,11 @@ module RSpock
|
|
|
30
30
|
statement_parser.parse(child)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
raises_count = spock_children.count { |c| c.type == :rspock_raises }
|
|
34
|
+
if raises_count > 1
|
|
35
|
+
raise BlockError, "Then block @ #{range} may contain at most one raises() condition"
|
|
36
|
+
end
|
|
37
|
+
|
|
33
38
|
s(:rspock_then, *spock_children)
|
|
34
39
|
end
|
|
35
40
|
end
|
|
@@ -117,16 +117,25 @@ module RSpock
|
|
|
117
117
|
|
|
118
118
|
def build_test_body(body_node, hoisted_setups)
|
|
119
119
|
body_children = []
|
|
120
|
+
blocks = body_node.children
|
|
120
121
|
|
|
121
|
-
|
|
122
|
+
blocks.each_with_index do |block_node, i|
|
|
122
123
|
case block_node.type
|
|
123
124
|
when :rspock_given
|
|
124
125
|
body_children.concat(block_node.children)
|
|
125
126
|
when :rspock_when
|
|
126
127
|
body_children.concat(hoisted_setups)
|
|
127
|
-
|
|
128
|
+
raises_node = find_raises_in_next_then(blocks, i)
|
|
129
|
+
|
|
130
|
+
if raises_node
|
|
131
|
+
body_children << build_assert_raises(block_node, raises_node)
|
|
132
|
+
else
|
|
133
|
+
body_children.concat(block_node.children)
|
|
134
|
+
end
|
|
128
135
|
when :rspock_then, :rspock_expect
|
|
129
|
-
|
|
136
|
+
block_node.children.each do |child|
|
|
137
|
+
body_children << child unless child.type == :rspock_raises
|
|
138
|
+
end
|
|
130
139
|
when :rspock_cleanup
|
|
131
140
|
# handled below as ensure
|
|
132
141
|
end
|
|
@@ -143,6 +152,31 @@ module RSpock
|
|
|
143
152
|
MethodCallToLVarTransformation.new(:_test_index_, :_line_number_).run(ast)
|
|
144
153
|
end
|
|
145
154
|
|
|
155
|
+
# --- Raises condition helpers ---
|
|
156
|
+
|
|
157
|
+
def find_raises_in_next_then(blocks, current_index)
|
|
158
|
+
next_block = blocks[current_index + 1]
|
|
159
|
+
return nil unless next_block&.type == :rspock_then
|
|
160
|
+
|
|
161
|
+
next_block.children.find { |c| c.type == :rspock_raises }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def build_assert_raises(when_node, raises_node)
|
|
165
|
+
when_body = when_node.children.length == 1 ? when_node.children[0] : s(:begin, *when_node.children)
|
|
166
|
+
|
|
167
|
+
assert_raises_call = s(:block,
|
|
168
|
+
s(:send, nil, :assert_raises, raises_node.exception_class),
|
|
169
|
+
s(:args),
|
|
170
|
+
when_body
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if raises_node.capture_name
|
|
174
|
+
s(:lvasgn, raises_node.capture_name, assert_raises_call)
|
|
175
|
+
else
|
|
176
|
+
assert_raises_call
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
146
180
|
# --- Where block helpers ---
|
|
147
181
|
|
|
148
182
|
def build_where_iterator(data_rows)
|
data/lib/rspock/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rspock
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jean-Philippe Duchesne
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-03-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|