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 +4 -4
- data/LICENSE.md +1 -1
- data/README.md +138 -237
- data/lib/r_spec/clone/dsl.rb +165 -189
- data/lib/r_spec/clone/error.rb +14 -6
- data/lib/r_spec/clone/expectation_helper/it.rb +19 -21
- data/lib/r_spec/clone/expectation_helper/matcher.rb +115 -0
- data/lib/r_spec/clone/expectation_helper.rb +3 -2
- data/lib/r_spec/clone/expectation_target.rb +50 -18
- data/lib/r_spec/clone/report.rb +65 -0
- data/lib/r_spec/clone.rb +1 -3
- data/lib/r_spec.rb +37 -109
- metadata +20 -29
- data/lib/r_spec/clone/error/pending_expectation.rb +0 -26
- data/lib/r_spec/clone/error/reserved_method.rb +0 -11
- data/lib/r_spec/clone/error/undefined_described_class.rb +0 -11
- data/lib/r_spec/clone/error/undefined_subject.rb +0 -11
- data/lib/r_spec/clone/expectation_helper/its.rb +0 -27
- data/lib/r_spec/clone/expectation_helper/shared.rb +0 -250
- data/lib/r_spec/clone/expectation_target/base.rb +0 -115
- data/lib/r_spec/clone/expectation_target/block.rb +0 -40
- data/lib/r_spec/clone/expectation_target/value.rb +0 -40
- data/lib/r_spec/clone/logger.rb +0 -42
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 937635b1d572b04258080f2feac11b64e75254f6c55a22bb409c98ae2f8ec5bc
|
|
4
|
+
data.tar.gz: 41a03037baa1aa2760a6e4a7661c0f0c9a069fb40691aa15dbf0d9153e5234ec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 209d5179db6fc57f98308589501106775287298c4c0457e67f1e19bc87eaf8e06663bbdf70b7dd8666dc495882631cfd39109ff155c6b8254de589e4c7917eea
|
|
7
|
+
data.tar.gz: 6b3fa674495e9090eb37a7c574f9bf1db4f44fa5c9052f0fdd520378fa90810893a12a7ef458de481a68efcee8f8cfe437f1e8b6001f1500d542d5b715fd4ef6
|
data/LICENSE.md
CHANGED
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
|
-

|
|
6
|
-
|
|
7
|
-
## Status
|
|
8
|
-
|
|
9
|
-
[](https://r-spec.dev/)
|
|
10
3
|
[](https://github.com/cyril/r_spec-clone.rb/tags)
|
|
11
4
|
[](https://rubydoc.info/github/cyril/r_spec-clone.rb/main)
|
|
12
|
-
[](https://github.com/cyril/r_spec-clone.rb/actions?query=workflow%
|
|
13
|
-
[](https://github.com/cyril/r_spec-clone.rb/actions?query=workflow%3Arubocop+branch%3Amain)
|
|
5
|
+
[](https://github.com/cyril/r_spec-clone.rb/actions?query=workflow%3ARuby+branch%3Amain)
|
|
14
6
|
[](https://github.com/cyril/r_spec-clone.rb/raw/main/LICENSE.md)
|
|
15
7
|
|
|
16
|
-
|
|
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
|
-
|
|
10
|
+

|
|
41
11
|
|
|
42
|
-
|
|
12
|
+
## Installation
|
|
43
13
|
|
|
44
14
|
```ruby
|
|
15
|
+
# Gemfile
|
|
45
16
|
gem "r_spec-clone"
|
|
46
17
|
```
|
|
47
18
|
|
|
48
|
-
|
|
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
|
-
[](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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
```
|
|
26
|
+
# spec/duck_spec.rb
|
|
27
|
+
require "r_spec"
|
|
118
28
|
|
|
119
|
-
|
|
29
|
+
class Duck
|
|
30
|
+
def walk = "Klop klop!"
|
|
31
|
+
def swim = "Swoosh..."
|
|
32
|
+
end
|
|
120
33
|
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
37
|
+
its(:walk) { is_expected.to eq "Klop klop!" }
|
|
38
|
+
its(:swim) { is_expected.to eq "Swoosh..." }
|
|
127
39
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
40
|
+
context "when asked to speak" do
|
|
41
|
+
its(:speak) { is_expected.to raise_exception NoMethodError }
|
|
42
|
+
end
|
|
131
43
|
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
49
|
+
pending "flies"
|
|
50
|
+
end
|
|
136
51
|
```
|
|
137
52
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
63
|
+
### Example groups
|
|
145
64
|
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
expect(actual).to be_false # passes if false.equal?(actual)
|
|
154
|
-
```
|
|
69
|
+
### Helpers
|
|
155
70
|
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
+
### Examples
|
|
163
76
|
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
117
|
+
class Duck
|
|
118
|
+
def quack = "Quack!"
|
|
119
|
+
end
|
|
219
120
|
|
|
220
|
-
|
|
121
|
+
RSpec.describe Duck do
|
|
122
|
+
let(:fix) { |name| Matchi::Fix.new(name) }
|
|
221
123
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
Many projects use a custom spec helper file, usually named `spec/spec_helper.rb`.
|
|
129
|
+
## Running specs
|
|
232
130
|
|
|
233
|
-
|
|
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
|
-
|
|
134
|
+
# Rakefile
|
|
241
135
|
require "rake/testtask"
|
|
242
136
|
|
|
243
137
|
Rake::TestTask.new do |t|
|
|
244
|
-
t.pattern = "spec
|
|
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
|
-
|
|
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
|
-
|
|
147
|
+
## Report
|
|
268
148
|
|
|
269
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
It's a great framework and it's a pleasure to work with every day.
|
|
181
|
+
## Differences from RSpec
|
|
294
182
|
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
+
## Changes in 2.0
|
|
300
198
|
|
|
301
|
-
[
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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.
|