r_spec-clone 1.7.2 → 2.0.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: 7312f229f5e837553c3bf3b36cb833f8ce1b2182f669e66b4c89bd0f38cd40ed
4
- data.tar.gz: c3fdc659bb67d21bf34e0d1e3e6c8bc7a0a277ffb7217c53e6762a2580863420
3
+ metadata.gz: 937635b1d572b04258080f2feac11b64e75254f6c55a22bb409c98ae2f8ec5bc
4
+ data.tar.gz: 41a03037baa1aa2760a6e4a7661c0f0c9a069fb40691aa15dbf0d9153e5234ec
5
5
  SHA512:
6
- metadata.gz: f4ebb556448f5bbed16b44a8c2ff643991dd36abf65540349a1b8159ceb6b5c97b2f1081a63f23a47627ccb02f83be45e9155dd5d2199a037372ea1f0516ebcc
7
- data.tar.gz: f9afb6e9aa7078f8e50ff715d5bc7eb5ac67b24e1038874dc453bd148f30a75581938fbec3a6cd3c8a809ac6b13cdd70ca91368f8208d02735a42a0ce353a8cf
6
+ metadata.gz: 209d5179db6fc57f98308589501106775287298c4c0457e67f1e19bc87eaf8e06663bbdf70b7dd8666dc495882631cfd39109ff155c6b8254de589e4c7917eea
7
+ data.tar.gz: 6b3fa674495e9090eb37a7c574f9bf1db4f44fa5c9052f0fdd520378fa90810893a12a7ef458de481a68efcee8f8cfe437f1e8b6001f1500d542d5b715fd4ef6
data/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # The MIT License
2
2
 
3
- Copyright (c) 2015-2024 Cyril Kato
3
+ Copyright (c) 2015-2026 Cyril Kato
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,317 +1,218 @@
1
1
  # RSpec clone <img src="https://github.com/cyril/r_spec-clone.rb/raw/main/img/logo.svg" style="height: 1em; width: 1em" />
2
2
 
3
- A minimalist __RSpec clone__ with all the essentials.
4
-
5
- ![What did you RSpec?](https://github.com/cyril/r_spec-clone.rb/raw/main/img/what-did-you-rspec.jpg)
6
-
7
- ## Status
8
-
9
- [![Home](https://img.shields.io/badge/Home-r--spec.dev-00af8b)](https://r-spec.dev/)
10
3
  [![Version](https://img.shields.io/github/v/tag/cyril/r_spec-clone.rb?label=Version&logo=github)](https://github.com/cyril/r_spec-clone.rb/tags)
11
4
  [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/cyril/r_spec-clone.rb/main)
12
- [![Ruby](https://github.com/cyril/r_spec-clone.rb/workflows/Ruby/badge.svg?branch=main)](https://github.com/cyril/r_spec-clone.rb/actions?query=workflow%3Aruby+branch%3Amain)
13
- [![RuboCop](https://github.com/cyril/r_spec-clone.rb/workflows/RuboCop/badge.svg?branch=main)](https://github.com/cyril/r_spec-clone.rb/actions?query=workflow%3Arubocop+branch%3Amain)
5
+ [![Ruby](https://github.com/cyril/r_spec-clone.rb/workflows/Ruby/badge.svg?branch=main)](https://github.com/cyril/r_spec-clone.rb/actions?query=workflow%3ARuby+branch%3Amain)
14
6
  [![License](https://img.shields.io/github/license/cyril/r_spec-clone.rb?label=License&logo=github)](https://github.com/cyril/r_spec-clone.rb/raw/main/LICENSE.md)
15
7
 
16
- ## Project Goals
17
-
18
- 1. Maintain low code complexity to avoid false negatives and false positives.
19
- 2. Implement the loading of specifications using simple, atomic, and thread-safe Ruby primitives.
20
- 3. Avoid cluttering the interface with unnecessary alternative syntaxes.
21
- 4. Provide the basics of the RSpec DSL for writing tests.
22
-
23
- ## Some Differences
24
-
25
- * Monkey-patching is not an available option.
26
- * The framework does not use [hacks such as the `at_exit` hook](https://blog.arkency.com/2013/06/are-we-abusing-at-exit/) to trigger tests.
27
- * Malicious _actual values_ cannot [compromise results](https://asciinema.org/a/423547?autoplay=1&speed=2).
28
- * If no `subject` is explicitly determined, it remains undefined.
29
- * If no described class is set, `described_class` is undefined rather than `nil`.
30
- * Expectations cannot be added inside a `before` block.
31
- * [Arbitrary helper methods](https://relishapp.com/rspec/rspec-core/v/3-10/docs/helper-methods/arbitrary-helper-methods) are not accessible within examples.
32
- * The `let` method defines a helper method rather than a memoized helper method.
33
- * The one-liner `is_expected` syntax is compatible with block expectations.
34
- * Definitions of `subject`, `before`, and `let` must precede examples.
35
- * The `after` hook is unsupported.
36
- * The execution of the test suite halts immediately when an error is detected.
37
- * Each `context` block isolates its tests and any potential side effects.
38
- * The `its` method is available without the need for external dependencies.
8
+ A minimalist clone of [RSpec](https://rspec.info/): the words of its language, `describe`, `context`, `it`, `its`, `let`, `subject`, `before`, `expect`, `is_expected`, `pending`, with a few matchers, and nothing else. The differences are deliberate: no monkey patch, no `at_exit` hook, no memoization, no `after`, a child process per `context`, the expected value always on the receiving end of a comparison, and a run that stops at the first expectation that is not met. What it does, it does in about 250 lines, on the testing libraries of [Fixrb](https://fixrb.dev/).
39
9
 
40
- ## Installation
10
+ ![What did you RSpec?](https://github.com/cyril/r_spec-clone.rb/raw/main/img/what-did-you-rspec.jpg)
41
11
 
42
- Add this line to your application's Gemfile:
12
+ ## Installation
43
13
 
44
14
  ```ruby
15
+ # Gemfile
45
16
  gem "r_spec-clone"
46
17
  ```
47
18
 
48
- And then execute:
49
-
50
- ```sh
51
- bundle install
52
- ```
53
-
54
- Or install it yourself as:
55
-
56
- ```sh
57
- gem install r_spec-clone
58
- ```
59
-
60
- ## Overview
61
-
62
- __RSpec clone__ provides a structure for writing executable examples of how your code should behave.
63
-
64
- Inspired by RSpec, it includes a domain-specific language (DSL) that allows you to write examples in a way similar to plain english.
65
-
66
- A basic spec looks something like this:
67
-
68
- [![RSpec clone demo](https://asciinema.org/a/422210.svg)](https://asciinema.org/a/422210?autoplay=1&speed=2)
19
+ RSpec clone requires Ruby 3.3 or later on a system with `fork`, and depends on [Defi](https://github.com/fixrb/defi), [Matchi](https://github.com/fixrb/matchi) and [Spectus](https://github.com/fixrb/spectus).
69
20
 
70
21
  ## Usage
71
22
 
72
- ### Anatomy of a Spec File
73
-
74
- To utilize the `RSpec` module and its DSL, include `require "r_spec"` in your spec files.
75
- Many projects organize these includes through a custom spec helper.
76
-
77
- Concrete test cases are defined in `it` blocks.
78
- An optional (but recommended) descriptive string or module indicates the purpose of the test and a block contains the main logic of the test.
79
-
80
- Test cases that have been defined or outlined but are not yet expected to work can be defined using `pending` instead of `it`.
81
- They will not be run but show up in the spec report as pending.
82
-
83
- An `it` block contains an example that should invoke the code to be tested and define what is expected of it.
84
- Each example can contain multiple expectations, but it should test only one specific behaviour.
85
-
86
- The `its` method can also be used to generate a nested example group with a single example that specifies the expected value (or the block expectations) of an attribute of the subject using `is_expected`.
87
-
88
- To express an expectation, wrap an object or block in `expect`, call `to` (or `not_to`) and pass it a matcher object.
89
- If the expectation is met, code execution continues.
90
- Otherwise the example has _failed_ and other code will not be executed.
91
-
92
- In test files, specs can be structured by example groups which are defined by `describe` and `context` sections.
93
- Typically a top level `describe` defines the outer unit (such as a class) to be tested by the spec.
94
- Further `describe` sections can be nested within the outer unit to specify smaller units under test (such as individual methods).
95
-
96
- For unit tests, it is recommended to follow the conventions for method names:
97
-
98
- * outer `describe` is the name of the class, inner `describe` targets methods;
99
- * instance methods are prefixed with `#`, class methods with `.`.
100
-
101
- To establish certain contexts — think _empty array_ versus _array with elements_ — the `context` method may be used to communicate this to the reader.
102
-
103
- Unlike a `describe` block, all specifications executed within a `context` are isolated in a subprocess.
104
- This prevents possible side effects on the Ruby object environment from being propagated outside their context, which could alter the result of the unit test suite.
105
-
106
- Note: if you are wondering what kind of code might be generated by the DSL, an article that shows the dynamic transcription of the main methods with simple examples is available in [Chinese](https://ruby-china.org/topics/41441), in [English](https://dev.to/cyri_/what-ruby-code-to-expect-from-a-testing-dsl-4oe1) and in [Japanese](https://qiita.com/cyril/items/17ee758e162bae144a07).
107
-
108
- ### Expectations
109
-
110
- Expectations define if the value being tested (_actual_) matches a certain value or specific criteria.
111
-
112
- #### Equivalence
23
+ A spec file requires `r_spec`, and its examples run as the file loads:
113
24
 
114
25
  ```ruby
115
- expect(actual).to eql(expected) # passes if expected.eql?(actual)
116
- expect(actual).to eq(expected) # passes if expected.eql?(actual)
117
- ```
26
+ # spec/duck_spec.rb
27
+ require "r_spec"
118
28
 
119
- #### Identity
29
+ class Duck
30
+ def walk = "Klop klop!"
31
+ def swim = "Swoosh..."
32
+ end
120
33
 
121
- ```ruby
122
- expect(actual).to equal(expected) # passes if expected.equal?(actual)
123
- expect(actual).to be(expected) # passes if expected.equal?(actual)
124
- ```
34
+ RSpec.describe Duck do
35
+ subject { described_class.new }
125
36
 
126
- #### Comparisons
37
+ its(:walk) { is_expected.to eq "Klop klop!" }
38
+ its(:swim) { is_expected.to eq "Swoosh..." }
127
39
 
128
- ```ruby
129
- expect(actual).to be_within(delta).of(expected) # passes if (expected - actual).abs <= delta
130
- ```
40
+ context "when asked to speak" do
41
+ its(:speak) { is_expected.to raise_exception NoMethodError }
42
+ end
131
43
 
132
- #### Regular expressions
44
+ it "walks before it swims" do
45
+ expect(subject.walk).to match(/Klop/)
46
+ expect { subject.swim }.not_to raise_exception StandardError
47
+ end
133
48
 
134
- ```ruby
135
- expect(actual).to match(expected) # passes if expected.match?(actual)
49
+ pending "flies"
50
+ end
136
51
  ```
137
52
 
138
- #### Expecting errors
139
-
140
- ```ruby
141
- expect { actual }.to raise_exception(expected) # passes if expected exception is raised
53
+ ```console
54
+ $ ruby spec/duck_spec.rb
55
+ spec/duck_spec.rb:12: Success: expected "Klop klop!" to eq "Klop klop!"
56
+ spec/duck_spec.rb:13: Success: expected "Swoosh..." to eq "Swoosh..."
57
+ spec/duck_spec.rb:16: Success: expected #<NoMethodError: undefined method 'speak' for an instance of Duck> to raise exception NoMethodError
58
+ spec/duck_spec.rb:20: Success: expected "Klop klop!" to match /Klop/
59
+ spec/duck_spec.rb:21: Success: expected "Swoosh..." not to raise exception StandardError
60
+ spec/duck_spec.rb:24: Pending: flies
142
61
  ```
143
62
 
144
- #### True
63
+ ### Example groups
145
64
 
146
- ```ruby
147
- expect(actual).to be_true # passes if true.equal?(actual)
148
- ```
65
+ `RSpec.describe` opens an example group; `describe` and `context` nest groups inside it. A group is a class, a nested group a subclass of it: what `let`, `subject` and `before` define in a group, the groups inside it inherit, and may redefine with `super` at hand. The first argument of `describe` is a description, or the module under test, which `described_class` then returns; where no enclosing `describe` names a module, `described_class` raises `RSpec::Clone::Error::UndefinedDescribedClass`.
149
66
 
150
- #### False
67
+ A `context` runs its specs in a child process, forked from the one that loads the file: what they change in the Ruby process, a constant, a global, the object a `subject` memoized by hand, stays in the child. The parent waits for the child, and exits with the status 1 when the child did not succeed.
151
68
 
152
- ```ruby
153
- expect(actual).to be_false # passes if false.equal?(actual)
154
- ```
69
+ ### Helpers
155
70
 
156
- #### Nil
71
+ `let(:name) { … }` defines a private method of the group, called each time it is used: nothing is memoized, so a `let` that must give the same object twice keeps it in an instance variable. The block takes parameters, as a method does. `subject { … }` is `let(:subject)`. The name `initialize`, kept for `before`, and the words of an example, `expect`, `is_expected` and the matchers, which the helper could not reach, raise `RSpec::Clone::Error::ReservedMethod`.
157
72
 
158
- ```ruby
159
- expect(actual).to be_nil # passes if nil.equal?(actual)
160
- ```
73
+ `before { … }` runs before each example of the group, after the blocks of the enclosing groups and those written before it in the group, in the example itself: an instance variable set there is seen by the example and by the helpers it calls.
161
74
 
162
- #### Type/class
75
+ ### Examples
163
76
 
164
- ```ruby
165
- expect(actual).to be_instance_of(expected) # passes if expected.equal?(actual.class)
166
- expect(actual).to be_an_instance_of(expected) # passes if expected.equal?(actual.class)
167
- ```
77
+ `it("description") { … }` runs its block in a new example at once, in the order of the file: an example is an instance of a class of its own, under the group, where `expect`, `is_expected` and the matchers are words. `its(:name, *arguments) { … }` does the same for one attribute of the subject: `is_expected` there wraps `name`, sent to `subject` with the arguments when the matcher asks for it, so that what it raises can be expected too. `pending "description"` writes a line and runs nothing. Where no enclosing group defines a `subject`, `subject` raises `RSpec::Clone::Error::UndefinedSubject`.
168
78
 
169
- #### Predicate
79
+ `expect(value)` wraps a value; `expect { … }` wraps a block, called when the matcher asks for its value, so that an exception can be expected; `is_expected` is `expect { subject }`. Then `to(matcher)` passes when the matcher matches, `not_to(matcher)` when it does not, and the result is written at once. An expectation that is not met ends the process: the rest of the file would run on a false premise.
170
80
 
171
- ```ruby
172
- expect(actual).to be_xxx # passes if actual.xxx?
173
- expect(actual).to be_have_xxx(:yyy) # passes if actual.has_xxx?(:yyy)
174
- ```
81
+ ### Matchers
175
82
 
176
- ##### Examples
83
+ Every matcher is one of [Matchi](https://github.com/fixrb/matchi), and the expected value is the one that receives the comparison: an actual value that lies about `==`, `eql?` or `equal?` cannot pass.
177
84
 
178
- ```ruby
179
- expect([]).to be_empty
180
- expect(foo: 1).to have_key(:foo)
181
- ```
85
+ | Matcher | Passes when |
86
+ |---|---|
87
+ | `eq(expected)`, `eql(expected)` | `expected.eql?(actual)`, and `actual` is of the class of `expected` |
88
+ | `be(expected)`, `equal(expected)` | `expected.equal?(actual)` |
89
+ | `be_true`, `be_false`, `be_nil` | `actual` is that very object |
90
+ | `be_an_instance_of(klass)` | `klass` is the class of `actual` |
91
+ | `be_a_kind_of(mod)` | `actual` is an instance of `mod` or of a descendant |
92
+ | `be_within(delta).of(expected)` | `(expected - actual).abs <= delta`, for an `Integer`, a `Float`, a `Rational` or a `BigDecimal` |
93
+ | `match(regexp)` | `regexp` matches `actual`, a `String` or a `Symbol` |
94
+ | `raise_exception(klass, message = nil)` | the block raises an instance of `klass`, with that message (a `String` to equal, or a `Regexp` to match) |
95
+ | `satisfy { \|actual\| … }` | the block answers `true`; a `TypeError` for anything but `true` or `false` |
96
+ | `be_xxx(*arguments)` | `actual.xxx?(*arguments)` answers `true`; a `TypeError` for anything but `true` or `false` |
97
+ | `have_xxx(*arguments)` | `actual.has_xxx?(*arguments)` answers `true`, likewise |
98
+ | `change(object, :method).by(delta)` | `object.method` changed by `delta` across the block |
99
+ | `change(object, :method).by_at_least(delta)` | changed by `delta` or more |
100
+ | `change(object, :method).by_at_most(delta)` | changed by `delta` or less |
101
+ | `change(object, :method).from(old).to(new)` | was `old`, became `new` |
102
+ | `change(object, :method).to(new)` | became `new`, from something else |
182
103
 
183
- #### Change
184
-
185
- ```ruby
186
- expect { object.action }.to change(object, :value).to(new)
187
- expect { object.action }.to change(object, :value).from(old).to(new)
188
- expect { object.action }.to change(object, :value).by(delta)
189
- expect { object.action }.to change(object, :value).by_at_least(minimum_delta)
190
- expect { object.action }.to change(object, :value).by_at_most(maximum_delta)
191
- ```
104
+ `be_xxx` and `have_xxx` are any name that starts with `be_` or `have_` and goes on with a letter; `klass` and `mod` can also be given by name.
192
105
 
193
- #### Satisfy
106
+ A matcher is any object with `match?`, which takes the block giving the actual value and answers `true` or `false`, and `to_s`, which describes it: `to` and `not_to` take it as it is. So a specification of [Fix](https://github.com/fixrb/fix) is a matcher through [matchi-fix](https://github.com/fixrb/matchi-fix), and a `let` with a parameter gives it a word:
194
107
 
195
108
  ```ruby
196
- expect(actual).to(satisfy { |value| value == expected })
197
- ```
198
-
199
- ### Running specs
200
-
201
- By convention, specs live in the `spec/` directory of a project. Spec files should end with `_spec.rb` to be recognizable as such.
202
-
203
- Depending of the project settings, you may run the specs of a project by running `rake spec` (see _Rake integration example_ section below).
204
- A single file can also be executed directly with the Ruby interpreter.
205
-
206
- #### Examples
109
+ require "r_spec"
110
+ require "fix"
111
+ require "matchi/fix"
207
112
 
208
- Run all specs in files matching `spec/**/*_spec.rb`:
209
-
210
- ```sh
211
- bundle exec rake spec
212
- ```
213
-
214
- Run a single file:
113
+ Fix :Duck do
114
+ on(:quack) { it MUST eq "Quack!" }
115
+ end
215
116
 
216
- ```sh
217
- ruby spec/my/test/file_spec.rb
218
- ```
117
+ class Duck
118
+ def quack = "Quack!"
119
+ end
219
120
 
220
- It is not recommended, but the RSpec's [`rspec` command line](https://relishapp.com/rspec/rspec-core/docs/command-line) might also work:
121
+ RSpec.describe Duck do
122
+ let(:fix) { |name| Matchi::Fix.new(name) }
221
123
 
222
- ```sh
223
- rspec spec/my/test/file_spec.rb
224
- rspec spec/my/test/file_spec.rb:42
225
- rspec spec/my/test/
226
- rspec
124
+ it { expect(Duck.new).to fix(:Duck) }
125
+ it { expect(Object.new).not_to fix(:Duck) }
126
+ end
227
127
  ```
228
128
 
229
- ### Spec helper
230
-
231
- Many projects use a custom spec helper file, usually named `spec/spec_helper.rb`.
129
+ ## Running specs
232
130
 
233
- This file is used to require `r_spec/clone` and other includes, like the code from the project needed for every spec file.
234
-
235
- ### Rake integration example
236
-
237
- The following `Rakefile` settings should be enough:
131
+ A spec file is a Ruby file: `ruby spec/duck_spec.rb` runs it, and its status is 0 when every expectation was met, 1 otherwise. An `exit` reached outside the block of an expectation, in a `before`, a `let`, the body of an `it` or a value given to `expect`, ends the run with its own status, as in RSpec, or the child of a `context`, whose status the parent reads: only `expect { … }`, `is_expected` and `its` judge it. A project runs its files with a Rake task:
238
132
 
239
133
  ```ruby
240
- require "bundler/gem_tasks"
134
+ # Rakefile
241
135
  require "rake/testtask"
242
136
 
243
137
  Rake::TestTask.new do |t|
244
- t.pattern = "spec/**/*_spec.rb"
138
+ t.pattern = File.join("spec", "**", "*_spec.rb")
245
139
  end
246
140
 
247
141
  task spec: :test
248
142
  task default: :test
249
143
  ```
250
144
 
251
- And then execute:
252
-
253
- ```sh
254
- bundle exec rake
255
- ```
256
-
257
- ## Performance
258
-
259
- The benchmarks compare the performance of [`r_spec-clone`](https://rubygems.org/gems/r_spec-clone) with the following frameworks (in alphabetical order):
260
-
261
- * [`fix`](https://rubygems.org/gems/fix)
262
- * [`minitest`](https://rubygems.org/gems/minitest)
263
- * [`rspec`](https://rubygems.org/gems/rspec)
264
-
265
- ### Boot time
145
+ The files then load one after the other in one process, so a spec helper, `spec/spec_helper.rb` say, that requires `r_spec` and the code under test is the usual way to share what every file needs.
266
146
 
267
- Benchmark against [100 executions of a file containing 1 expectation](https://github.com/cyril/r_spec-clone.rb/blob/main/benchmark/boot_time/) (lower is better).
147
+ ## Report
268
148
 
269
- ![Boot time benchmark](https://r-spec.dev/benchmark-boot-time.svg)
149
+ For each expectation, one line: its location in the file, the outcome, and the summary of [Spectus](https://github.com/fixrb/spectus), `expected ACTUAL to DESCRIPTION`, with `not` for a negation, or `CLASS: MESSAGE` for an exception that the matcher did not expect. A line break in either is written `\n`. A pending example gives `LOCATION: Pending: DESCRIPTION`.
270
150
 
271
- ### Runtime
272
-
273
- Benchmark against [1 execution of a file containing 100,000 expectations](https://github.com/cyril/r_spec-clone.rb/blob/main/benchmark/run_time/) (lower is better).
274
-
275
- ![Runtime benchmark](https://r-spec.dev/benchmark-run-time.svg)
276
-
277
- ## Test suite
278
-
279
- __RSpec clone__'s specifications are self-described here: [spec/](https://github.com/cyril/r_spec-clone.rb/blob/main/spec/)
280
-
281
- ## Contact
151
+ ```ruby
152
+ # spec/account_spec.rb
153
+ require "r_spec"
154
+ require_relative "../lib/account"
155
+
156
+ RSpec.describe Account do
157
+ subject { described_class.new(100) }
158
+
159
+ its(:balance) { is_expected.to eq 100 }
160
+ pending "pays interest"
161
+ its(:withdraw, 500) { is_expected.to raise_exception ArgumentError }
162
+ its(:withdraw, 500) { is_expected.to eq(-400) }
163
+ its(:balance) { is_expected.to eq 200 }
164
+ end
165
+ ```
282
166
 
283
- * Home page: [https://r-spec.dev/](https://r-spec.dev/)
284
- * Cheatsheet: [https://r-spec.dev/cheatsheet.html](https://r-spec.dev/cheatsheet.html)
285
- * Blog post: [https://cyrilllllll.medium.com/introducing-a-new-rspec-850d48c0f901](https://cyrilllllll.medium.com/introducing-a-new-rspec-850d48c0f901)
286
- * Source code: [https://github.com/cyril/r_spec-clone.rb](https://github.com/cyril/r_spec-clone.rb)
287
- * API Doc: [https://rubydoc.info/gems/r_spec-clone](https://rubydoc.info/gems/r_spec-clone)
288
- * Twitter: [https://twitter.com/cyri\_](https://twitter.com/cyri\_)
167
+ ```console
168
+ $ ruby spec/account_spec.rb
169
+ spec/account_spec.rb:8: Success: expected 100 to eq 100
170
+ spec/account_spec.rb:9: Pending: pays interest
171
+ spec/account_spec.rb:10: Success: expected #<ArgumentError: insufficient funds> to raise exception ArgumentError
172
+ spec/account_spec.rb:11: Error: ArgumentError: insufficient funds
173
+ /home/alice/account/lib/account.rb:9:in 'Account#withdraw'
174
+ spec/account_spec.rb:11:in 'block (2 levels) in <main>'
175
+ spec/account_spec.rb:11:in 'block in <main>'
176
+ spec/account_spec.rb:5:in '<main>'
177
+ ```
289
178
 
290
- ## Special thanks ❤️
179
+ A success goes to the standard output. A failure, when the matcher answered the wrong way, and an error, when the block raised something the matcher did not expect, go to the standard error, the error with the frames of its backtrace that are not in RSpec clone, Defi, Matchi or Spectus, and end the process with the status 1: here, the last expectation never runs. Nothing else is written: no letter, no colour, no count.
291
180
 
292
- I would like to thank the whole [RSpec team](https://rspec.info/about/) for all their work.
293
- It's a great framework and it's a pleasure to work with every day.
181
+ ## Differences from RSpec
294
182
 
295
- Without RSpec, this clone would not have been possible.
183
+ Some of RSpec is left out on purpose; the rest works as written above. If you come from RSpec, these will surprise you:
296
184
 
297
- ## Buy me a coffee ☕
185
+ - Nothing is monkey-patched, nothing runs from an `at_exit` hook: the examples run as the file loads, in its order, and there is no `rspec` command, no filter, no random order, no formatter.
186
+ - The run stops at the first expectation that is not met.
187
+ - `let` and `subject` are plain methods, called each time: no memoization, no `let!`, no `subject!`. Definitions must precede the examples that use them, as in a class.
188
+ - `before` is the only hook: no `after`, no `around`, no `before(:all)`.
189
+ - `subject` and `described_class` are undefined, not `nil`, until a group defines them.
190
+ - `its` is built in, and its argument is a Symbol, followed by the arguments of the method.
191
+ - A `context` runs in a child process; a `describe` does not.
192
+ - `expect` takes a value or a block, never both, and `to` takes a matcher object: no block form of `change`, no `and`, `or`, `all`, `not_to raise_error` without a class, no custom matcher DSL. A matcher is any object with `match?` and `to_s`.
193
+ - `eq` is RSpec's `eql`: `1` is not `1.0`. `match` takes a `Regexp` only. `raise_exception` is `raise_error`, and requires an exception class, or its name; it does not rescue an `Interrupt`, a signal, a `NoMemoryError` or the exception with which an enclosing `Timeout` ends its block, unless that is the class expected. `be_truthy`, `be_falsey`, `include`, `start_with`, `respond_to` and the rest do not exist, and `be_a(klass)` is the predicate `a?`, which no object has: `be_true`, `be_false`, `be_a_kind_of`, a predicate or `satisfy` say it.
194
+ - A predicate matcher calls a public method of the actual value, and that method must answer `true` or `false`.
195
+ - `change(object, :method).to(new)` demands a change, and `change` raises a `TypeError` when the observed method returns the same unfrozen object twice: observe a value, not the mutated object.
298
196
 
299
- If you like this project, please consider making a small donation.
197
+ ## Changes in 2.0
300
198
 
301
- [![Donate](https://img.shields.io/badge/Donate-cyr.eth-purple.svg)](https://etherscan.io/address/cyr.eth)
199
+ - Built on [Spectus](https://github.com/fixrb/spectus) 6 and [Matchi](https://github.com/fixrb/matchi) 6, with [Defi](https://github.com/fixrb/defi) 4 for `its`, in place of test_tube and expresenter: every expectation is a MUST of Spectus, judged as Fix 1.0 judges its own. Ruby 3.3 or later.
200
+ - The report is one line per expectation, `LOCATION: OUTCOME: SUMMARY`, where the location is the line of the expectation, not of the example, and the summary is Spectus', with the actual value and without a final period; the frames of an error follow it. No colour, no emoji. A pending example reads `Pending`, not `Warning`. The standard output is flushed before a failure is written on the standard error.
201
+ - An exception the matcher did not expect is an error, written with its class and message and followed by its frames. An `Interrupt`, a signal or a `NoMemoryError` raised in the block goes through, where 1.x made it an error.
202
+ - `be_a_kind_of` is a matcher, and `raise_exception` takes an expected message.
203
+ - The changes of Matchi 5 and 6 apply: `eq` requires the actual value to be of the class of the expected one, `match` takes a `Regexp` only, `satisfy` and the predicates must answer `true` or `false`, a predicate reaches public methods only, `change … to` requires a change, `change` raises on the same unfrozen object twice, `be_within` subtracts core numbers only, and `raise_exception` lets an `Interrupt`, a signal, a `NoMemoryError` or a `Timeout` go through.
204
+ - `describe`, `context`, `before`, `it` and `its` without a block, and `its` with an attribute that is not a Symbol, raise `ArgumentError`. `let` refuses the words of an example, which the helper could not reach. Several `before` in one group run in their order, where 1.x kept the last.
205
+ - A child process that the block of an expectation forked and let run on leaves the clone without a word, with `exit!`, instead of writing a result of its own.
206
+ - `RSpec::Clone::Error::PendingExpectation`, `RSpec::Clone::Logger`, `RSpec::Clone::ExpectationTarget::Base`, `Block` and `Value`, `RSpec::Clone::ExpectationHelper::Shared` and `Its` are gone: `RSpec::Clone::ExpectationTarget` is one class, `RSpec::Clone::ExpectationHelper::Matcher` holds the matchers, and `RSpec::Clone::Report` writes the lines. The site and the benchmarks are gone from the repository.
302
207
 
303
208
  ## Versioning
304
209
 
305
- __RSpec clone__ follows [Semantic Versioning 2.0](https://semver.org/).
210
+ RSpec clone follows [Semantic Versioning 2.0](https://semver.org/).
306
211
 
307
212
  ## License
308
213
 
309
214
  The [gem](https://rubygems.org/gems/r_spec-clone) is available as open source under the terms of the [MIT License](https://github.com/cyril/r_spec-clone.rb/raw/main/LICENSE.md).
310
215
 
311
- ## One more thing
312
-
313
- Under the hood, __RSpec clone__ is largely animated by [a collection of testing libraries designed to make programmers happy](https://github.com/fixrb/).
314
-
315
- It's a living example of what we can do combining small libraries together that can boost the fun of programming.
216
+ ## Thanks
316
217
 
317
- ![Fix testing tools logo for Ruby](https://github.com/cyril/r_spec-clone.rb/raw/main/img/fixrb.svg)
218
+ To the [RSpec team](https://rspec.info/about/), for the language this clone borrows, and for a framework it is a pleasure to work with every day.