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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e4d05a65bb9d040615d65ceee976bc3b0fef696d54921b9fb47160c07c18dc2
4
- data.tar.gz: 8d2b4a392f6ea3cff0133278a7994b2934fea9d2a3d695da4a9da66d2727ae25
3
+ metadata.gz: d6c10b52d342827b092af647c5bc144f4e381dff91167eedf93a0f99c9488ee2
4
+ data.tar.gz: 20ea274b4fa0e3a79eefb88f33c53ddf84adfc582fd37fe3e2ee9688a8137747
5
5
  SHA512:
6
- metadata.gz: c59e0798461d56be30b9579cd09b72b373a2b70039139295cc42bf610031e73be438e93f0645963cfb29d6617cab0096b0de978a954950ec6f9c21c13e1d510a
7
- data.tar.gz: 2d36558c145a2684e169331d7f277f2aa7b09b39c642b331dcf4ec36757228a0ccf6c8ab1ce5b8be7408b157061ae30ef0b7a45108ea012a1462742cbb480b87
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.4.0...HEAD
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rspock (2.4.0)
4
+ rspock (2.5.0)
5
5
  ast_transform (~> 2.0)
6
6
  minitest (~> 5.0)
7
7
  mocha (>= 1.0)
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
- # :rspock_returns -> .returns(value)
14
- # :rspock_raises -> .raises(exception_class, ...)
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
- rspock_returns: :returns,
20
- rspock_raises: :raises,
19
+ rspock_stub_returns: :returns,
20
+ rspock_stub_raises: :raises,
21
21
  }.freeze
22
22
 
23
23
  def initialize(index = 0)
@@ -73,12 +73,20 @@ module RSpock
73
73
  class OutcomeNode < Node
74
74
  end
75
75
 
76
- class ReturnsNode < OutcomeNode
77
- register :rspock_returns
76
+ class StubReturnsNode < OutcomeNode
77
+ register :rspock_stub_returns
78
78
  end
79
79
 
80
- class RaisesNode < OutcomeNode
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(:rspock_returns, value) or s(:rspock_raises, *args)
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(:rspock_raises, *node.children[2..])
66
+ s(:rspock_stub_raises, *node.children[2..])
67
67
  else
68
- s(:rspock_returns, node)
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
- body_node.children.each do |block_node|
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
- body_children.concat(block_node.children)
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
- body_children.concat(block_node.children)
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)
@@ -1,3 +1,3 @@
1
1
  module RSpock
2
- VERSION = "2.4.0"
2
+ VERSION = "2.5.0"
3
3
  end
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.0
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-02-28 00:00:00.000000000 Z
11
+ date: 2026-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler