Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 127 additions & 19 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,154 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [UNRELEASED]
## [Unreleased]

### Added

- Spock-style implicit assertions: every non-assignment statement in Then/Expect blocks is now an assertion — no assertion API needed.
- Binary operator assertions: `=~`, `!~`, `>`, `<`, `>=`, `<=` with clear error messages.
- General statement assertions: bare boolean expressions (e.g. `obj.valid?`) with the original source text in the error message.
- Negation support: `!expr` is detected automatically and produces a clear error message.
- `>> raises(...)` syntax for exception stubbing in interactions.

### Changed

- Renamed `ConditionParser` to `StatementParser` and `ConditionToAssertionTransformation` to `StatementToAssertionTransformation` for consistency with Spock's model.
- Then and Expect block parsers now use `StatementParser` for statement classification.

### Removed

- `ComparisonToAssertionTransformation` — replaced by `StatementToAssertionTransformation`.

## [2.3.1] - 2026-02-27

### Fixed

- Require `block_capture` so it is available at runtime.

## [2.3.0] - 2026-02-27

### Added

- Interaction transformations and block identity verification via `&` operator.
- RSpock AST node hierarchy (`Node`, `InteractionNode`, `BodyNode`, etc.) for type-safe AST handling.
- `TestMethodParser` extracted from `TestMethodTransformation` for separation of parsing and transformation.

### Changed

- Restructured block classes into `Parser` namespace and converted `InteractionParser` to a class.
- Introduced `BodyNode` and removed legacy interaction transformations.

## [2.2.0] - 2026-02-25

### Added

- Interaction stubbing with `>>` for return value stubbing in Then block interactions.

### Fixed

- Pry and pry-byebug compatibility.
- Failing test on Ruby 3+.
- `filter_string` for `ast_transform` 2.1.4 source mapping change.

## [2.1.0] - 2026-02-21

### Added

- Ruby 4.0 support.

### Fixed

- Codecov badge URL to use master branch.

## [2.0.0] - 2026-02-21

### Changed

- Minimum Ruby version bumped to 3.2.
- Upgraded to Ruby 3.x compatibility.
- Use `ast_transform` 2.0.0 from RubyGems.
- CI modernization and release workflow improvements.

## [1.0.0] - 2020-07-09

### Added
- Interaction-based testing: Mock with expectations in the Then block.

- Interaction-based testing: mock with expectations in the Then block.
- Travis CI and code coverage.

### Changed

- Test names now have the test index and line number as suffix instead of prefix.
- Cleanup transformed code output.
- Removed unnecessary ensure block when Cleanup block is empty; moved source map wrapper to class scope.
- Bump `ast_transform` to release 1.0.0.

### Fixed
- Fixed source mapping for transformed assertion nodes.

## [0.2.5] 2019-05-28
- Source mapping for transformed assertion nodes.
- Truth table generator command with proper escaping.

## [0.2.5] - 2019-05-28

### Fixed
- Fixed BacktraceFilter so that source mapping works again

## [0.2.4] 2019-05-27
- BacktraceFilter so that source mapping works again.

## [0.2.4] - 2019-05-27

### Changed
- Bump Unparser dependency from ~> 0.2.8 to ~> 0.4

## [0.2.3] 2018-11-09
- Bump Unparser dependency from `~> 0.2.8` to `~> 0.4`.

## [0.2.3] - 2018-11-09

### Fixed
- Cleanup block can now contain more than one node

## [0.2.2] 2018-11-08
- Cleanup block can now contain more than one node.

## [0.2.2] - 2018-11-08

### Changed
- Extracted ASTTransform to its own gem: `ast_transform`

## [0.2.1] 2018-10-09
- Extracted ASTTransform to its own gem: `ast_transform`.

## [0.2.1] - 2018-10-09

### Added
- _line_number_ is now displayed in the test name, and is available in test scope for debugging purposes

- `_line_number_` is now displayed in the test name and available in test scope for debugging.

### Changed
- Renamed test_index to _test_index_

## [0.2.0] 2018-09-21
- Renamed `test_index` to `_test_index_`.

## [0.2.0] - 2018-09-21

### Added

- Truth table generator Rake task.

## [0.1.1] 2018-09-19
### Initial Release!
## [0.1.1] - 2018-09-18

### Added

- Initial release.

[Unreleased]: https://github.com/rspockframework/rspock/compare/v2.3.1...HEAD
[2.3.1]: https://github.com/rspockframework/rspock/compare/v2.3.0...v2.3.1
[2.3.0]: https://github.com/rspockframework/rspock/compare/v2.2.0...v2.3.0
[2.2.0]: https://github.com/rspockframework/rspock/compare/v2.1.0...v2.2.0
[2.1.0]: https://github.com/rspockframework/rspock/compare/v2.0.0...v2.1.0
[2.0.0]: https://github.com/rspockframework/rspock/compare/1.0.0...v2.0.0
[1.0.0]: https://github.com/rspockframework/rspock/compare/0.2.5...1.0.0
[0.2.5]: https://github.com/rspockframework/rspock/compare/0.2.4...0.2.5
[0.2.4]: https://github.com/rspockframework/rspock/compare/0.2.3...0.2.4
[0.2.3]: https://github.com/rspockframework/rspock/compare/0.2.2...0.2.3
[0.2.2]: https://github.com/rspockframework/rspock/compare/0.2.1...0.2.2
[0.2.1]: https://github.com/rspockframework/rspock/compare/0.2.0...0.2.1
[0.2.0]: https://github.com/rspockframework/rspock/compare/0.1.1...0.2.0
[0.1.1]: https://github.com/rspockframework/rspock/releases/tag/0.1.1
49 changes: 38 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ Note: RSpock is heavily inspired by Spock for the Groovy programming language.

* BDD-style code blocks: Given, When, Then, Expect, Cleanup, Where
* Data-driven testing with incredibly expressive table-based Where blocks
* Expressive assertions: Use familiar comparison operators `==` and `!=` for assertions!
* Spock-style implicit assertions: every statement in Then/Expect blocks is an assertion (no assertion API needed)
* Binary operator assertions: `==`, `!=`, `=~`, `!~`, `>`, `<`, `>=`, `<=` with clear error messages
* General statement assertions: bare boolean expressions (e.g. `obj.valid?`, `list.include?(x)`) and negation (`!obj.empty?`) with source-text error messages
* [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`
* (Planned) BDD-style custom reporter that outputs information from Code Blocks
* (Planned) Capture all Then block violations
Expand Down Expand Up @@ -150,15 +152,25 @@ The When block describes the stimulus to be applied to the system under test. It

```ruby
Then "The product is added to the cart"
!cart.products.empty?
cart.products.size == 1
cart.products.first == product
cart.products.size > 0
```

The Then block describes the response from the stimulus. Any comparison operators used in the Then block (`==` or `!=`) is transformed to assert_equal / refute_equal under the hood. By convention, the __LHS__ operand is considered the __actual__ value, while the __RHS__ operand is considered the __expected__ value.
The Then block describes the response from the stimulus. Following Spock's core model, **every statement is an assertion** unless it's a variable assignment. No assertion API is needed.

**Binary operators** (`==`, `!=`, `=~`, `!~`, `>`, `<`, `>=`, `<=`) produce clear error messages on failure. By convention, the **LHS** operand is the **actual** value and the **RHS** is the **expected** value.

**General statements** (bare boolean expressions like `obj.valid?`, `list.include?(x)`) include the original source text in the error message, so you see exactly which expression failed. **Negation** (`!expr`) is detected automatically.

**Variable assignments** pass through unchanged and execute in source order after the stimulus.

#### Expect Block

The Expect block is useful when expressing the stimulus and the response in one statement is more natural. For example, let's compare two equivalent ways of describing some behaviour:
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.

For example, let's compare two equivalent ways of describing some behaviour:

##### When + Then
```ruby
Expand All @@ -175,6 +187,17 @@ Expect "absolute of -2 is 2"
-2.abs == 2
```

The Expect block supports the full range of assertion expressions:

```ruby
Expect "string matching and predicates"
str =~ /potato/
str.include?("pot")
!str.empty?
str.length > 3
str.length == 6
```

A good rule of thumb is using When + Then blocks to describe methods with side-effects and Expect blocks to describe purely functional methods.

#### Cleanup Block
Expand Down Expand Up @@ -560,20 +583,24 @@ To install this gem onto your local machine, run `bundle exec rake install`.

## Releasing a New Version

There are two ways to create a release. Both require that `version.rb` has already been updated and merged to main.
There are two ways to create a release. Both require that `version.rb` has already been updated, `CHANGELOG.md` has been updated, and changes have been merged to main.

### Via GitHub UI

1. Update `VERSION` in `lib/rspock/version.rb` and run `bundle install` to regenerate `Gemfile.lock`, commit, open a PR, and merge to main
2. Go to the repo on GitHub → **Releases** → **Draft a new release**
3. Enter a new tag (e.g. `v2.0.0`), select `main` as the target branch
4. Add a title and release notes (GitHub can auto-generate these from merged PRs)
5. Click **Publish release**
1. Update `VERSION` in `lib/rspock/version.rb` and run `bundle install` to regenerate `Gemfile.lock`
2. Move the `[Unreleased]` section in `CHANGELOG.md` to a new version heading with today's date (e.g. `## [2.4.0] - 2026-03-01`) and add a fresh empty `[Unreleased]` section above it. Update the comparison links at the bottom of the file.
3. Commit, open a PR, and merge to main
4. Go to the repo on GitHub → **Releases** → **Draft a new release**
5. Enter a new tag (e.g. `v2.4.0`), select `main` as the target branch
6. Add a title and release notes (GitHub can auto-generate these from merged PRs)
7. Click **Publish release**

### Via CLI

1. Update `VERSION` in `lib/rspock/version.rb` and run `bundle install` to regenerate `Gemfile.lock`, commit, open a PR, and merge to main
2. Tag and push:
1. Update `VERSION` in `lib/rspock/version.rb` and run `bundle install` to regenerate `Gemfile.lock`
2. Move the `[Unreleased]` section in `CHANGELOG.md` to a new version heading with today's date and add a fresh empty `[Unreleased]` section above it. Update the comparison links at the bottom of the file.
3. Commit, open a PR, and merge to main
4. Tag and push:
```
git checkout main && git pull
git tag v2.0.0
Expand Down
40 changes: 0 additions & 40 deletions lib/rspock/ast/comparison_to_assertion_transformation.rb

This file was deleted.

15 changes: 15 additions & 0 deletions lib/rspock/ast/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,21 @@ def outcome = children[4]
def block_pass = children[5]
end

class BinaryStatementNode < Node
register :rspock_binary_statement

def lhs = children[0]
def operator = children[1]
def rhs = children[2]
end

class StatementNode < Node
register :rspock_statement

def expression = children[0]
def source = children[1]
end

module NodeBuilder
include ASTTransform::TransformationHelper

Expand Down
7 changes: 7 additions & 0 deletions lib/rspock/ast/parser/expect_block.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true
require 'rspock/ast/parser/block'
require 'rspock/ast/parser/statement_parser'

module RSpock
module AST
Expand All @@ -20,6 +21,12 @@ def can_end?
def successors
@successors ||= [:Cleanup, :Where].freeze
end

def to_rspock_node
statement_parser = StatementParser.new
spock_children = @children.map { |child| statement_parser.parse(child) }
s(:rspock_expect, *spock_children)
end
end
end
end
Expand Down
48 changes: 48 additions & 0 deletions lib/rspock/ast/parser/statement_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'rspock/ast/node'

module RSpock
module AST
module Parser
# Classifies raw Ruby AST statements into RSpock node types for Then/Expect blocks.
#
# - Assignments pass through as raw AST (no wrapping).
# - Binary operators (==, !=, =~, etc.) become :rspock_binary_statement nodes.
# - Everything else becomes :rspock_statement nodes with the original source text captured.
class StatementParser
include RSpock::AST::NodeBuilder

BINARY_OPERATORS = %i[== != =~ !~ > < >= <=].freeze
ASSIGNMENT_TYPES = %i[lvasgn masgn op_asgn or_asgn and_asgn].freeze

def parse(node)
return node if assignment?(node)
return build_binary_statement(node) if binary_statement?(node)

build_statement(node)
end

private

def assignment?(node)
ASSIGNMENT_TYPES.include?(node.type)
end

def binary_statement?(node)
node.type == :send &&
node.children.length == 3 &&
BINARY_OPERATORS.include?(node.children[1])
end

def build_binary_statement(node)
s(:rspock_binary_statement, node.children[0], s(:sym, node.children[1]), node.children[2])
end

def build_statement(node)
source = node.loc&.expression&.source || node.inspect
s(:rspock_statement, node, s(:str, source))
end
end
end
end
end
Loading