fix 1.0.0.beta12 → 1.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 +2 -2
- data/README.md +183 -210
- data/lib/fix/dsl.rb +149 -97
- data/lib/fix/environment.rb +25 -0
- data/lib/fix/level.rb +40 -0
- data/lib/fix/matcher.rb +92 -41
- data/lib/fix/node.rb +32 -0
- data/lib/fix/result.rb +36 -0
- data/lib/fix/spec.rb +167 -0
- data/lib/fix.rb +42 -12
- data/lib/kernel.rb +10 -31
- metadata +35 -19
- data/lib/fix/doc.rb +0 -24
- data/lib/fix/requirement.rb +0 -87
- data/lib/fix/run.rb +0 -55
- data/lib/fix/set.rb +0 -67
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56afffa92641798956f66d8015ffaa394b229ca6a7a1af4f73aef233089b3d94
|
|
4
|
+
data.tar.gz: b6aa5f4f2cd7674d38e08ac78b3f53def1996760274c478676e1619dae5e809a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e50bb8c771b9bed55871df7d4115e7a0a50c3661ee09e86a82e6cc3def766c867f964e4f949c75d1a8191345918629ace15fad600d9906d6bf829ac9d31b3be5
|
|
7
|
+
data.tar.gz: 969531acec4e9f78a0df7641bfde3ca272bccc7cc038b2866e6ce406c9f6b3e339a73889a4ace65dd281d4663610a7559bcca289b1578c757e509e2e254f4ffd
|
data/LICENSE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2014-
|
|
3
|
+
Copyright (c) 2014-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,318 +1,291 @@
|
|
|
1
|
-
# Fix
|
|
1
|
+
# Fix
|
|
2
2
|
|
|
3
|
-
[](https://fixrb.dev/)
|
|
4
3
|
[](https://github.com/fixrb/fix/tags)
|
|
5
4
|
[](https://rubydoc.info/github/fixrb/fix/main)
|
|
6
5
|
[](https://github.com/fixrb/fix/raw/main/LICENSE.md)
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Fix is a modern Ruby testing framework that emphasizes clear separation between specifications and examples. Unlike traditional testing frameworks, Fix focuses on creating pure specification documents that define expected behaviors without mixing in implementation details.
|
|
7
|
+
Fix writes a specification: what a subject MUST, SHOULD or MAY do, with the words of [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119), and nothing about how the subject is made. The specification is then tested against a subject, each context in a child process, and every expectation gets a result.
|
|
11
8
|
|
|
12
9
|
## Installation
|
|
13
10
|
|
|
14
|
-
Add to your Gemfile:
|
|
15
|
-
|
|
16
11
|
```ruby
|
|
17
|
-
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem "fix"
|
|
18
14
|
```
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
Fix requires Ruby 3.3 or later on a system with `fork`, and depends on [Aw](https://github.com/fixrb/aw), [Defi](https://github.com/fixrb/defi), [Matchi](https://github.com/fixrb/matchi) and [Spectus](https://github.com/fixrb/spectus).
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
bundle install
|
|
24
|
-
```
|
|
18
|
+
## Usage
|
|
25
19
|
|
|
26
|
-
|
|
20
|
+
A file writes a specification, then tests a subject against it:
|
|
27
21
|
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
```ruby
|
|
23
|
+
# duck_fix.rb
|
|
24
|
+
require "fix"
|
|
31
25
|
|
|
32
|
-
|
|
26
|
+
class Duck
|
|
27
|
+
def walk = "Klop klop!"
|
|
28
|
+
def swim = "Swoosh..."
|
|
29
|
+
end
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
Fix :Duck do
|
|
32
|
+
on :walk do
|
|
33
|
+
it MUST eq "Klop klop!"
|
|
34
|
+
end
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
on :swim do
|
|
37
|
+
it MUST eq "Swoosh..."
|
|
38
|
+
end
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
on :speak do
|
|
41
|
+
it MAY eq "Quaaaaaack!"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
exit(Fix[:Duck].test { Duck.new })
|
|
46
|
+
```
|
|
41
47
|
|
|
42
|
-
|
|
48
|
+
```text
|
|
49
|
+
duck_fix.rb:11: Success: expected "Klop klop!" to eq "Klop klop!"
|
|
50
|
+
duck_fix.rb:15: Success: expected "Swoosh..." to eq "Swoosh..."
|
|
51
|
+
duck_fix.rb:19: Info: NoMethodError: undefined method 'speak' for an instance of Duck
|
|
52
|
+
```
|
|
43
53
|
|
|
44
|
-
|
|
54
|
+
`Fix :Duck do … end` builds the specification and registers it under its name; `Fix[:Duck]` finds it again. `test` builds a new subject with the block for every expectation, sends it the methods of the `on` around it, judges what comes out, writes one line per expectation, the backtrace of an error below it, and returns whether all of them pass (`true` when there is nothing to test). `exit` turns that into the status of the process: 0 when the specification is met, 1 otherwise.
|
|
45
55
|
|
|
46
|
-
|
|
56
|
+
Keep the parentheses: `exit Fix[:Duck].test do … end` gives the block to `exit`, and `test` raises `ArgumentError` for want of one. A file with several specifications tests them all before it exits, as `exit([Fix[:A].test { … }, Fix[:B].test { … }].all?)` does.
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
Fix do
|
|
50
|
-
let(:name) { "Bob" }
|
|
51
|
-
let(:age) { 42 }
|
|
58
|
+
## Writing a specification
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
end
|
|
55
|
-
```
|
|
60
|
+
The block of `Fix` is a document, read once when the file is loaded, with four words: `let`, `with`, `on` and `it`. Each `with`, each `on` opens a context, which inherits from the one around it.
|
|
56
61
|
|
|
57
|
-
###
|
|
62
|
+
### `on`
|
|
58
63
|
|
|
59
|
-
|
|
64
|
+
`on :name, *arguments, **keywords` sends a method to the subject, and the expectations inside judge what it returns. The `on` inside an `on` send their method to that value, and so on; with no `on` around, an expectation judges the subject itself.
|
|
60
65
|
|
|
61
66
|
```ruby
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
require "fix"
|
|
68
|
+
|
|
69
|
+
Fix :Answer do
|
|
70
|
+
it MUST be_an_instance_of Integer
|
|
71
|
+
|
|
72
|
+
on :+, 2 do
|
|
73
|
+
it MUST eq 44
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
on :to_s do
|
|
77
|
+
on :size do
|
|
78
|
+
it MUST eq 2
|
|
79
|
+
end
|
|
65
80
|
end
|
|
66
81
|
|
|
67
|
-
|
|
68
|
-
it
|
|
82
|
+
on :digits do
|
|
83
|
+
it MUST eq [2, 4]
|
|
69
84
|
end
|
|
70
85
|
end
|
|
86
|
+
|
|
87
|
+
exit(Fix[:Answer].test { 42 })
|
|
71
88
|
```
|
|
72
89
|
|
|
73
|
-
|
|
90
|
+
```text
|
|
91
|
+
example.rb:4: Success: expected 42 to be an instance of Integer
|
|
92
|
+
example.rb:7: Success: expected 44 to eq 44
|
|
93
|
+
example.rb:12: Success: expected 2 to eq 2
|
|
94
|
+
example.rb:17: Success: expected [2, 4] to eq [2, 4]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A method that raises ends the chain there, and the expectation judges the exception: an error for most matchers, what `raise_exception` is for. There is no helper for a predicate: `on(:empty?) { it MUST be_true }` asks the question. The method is sent without a block, since the block of `on` is the context: a method that needs one is called in the subject block, or tested with `satisfy`. A `BasicObject` has no `public_send`, so an `on` sent to it is an error.
|
|
98
|
+
|
|
99
|
+
### `let` and `with`
|
|
74
100
|
|
|
75
|
-
|
|
101
|
+
A `let` is a parameter of the subject: a name and a block, for the block of `test` to call when it builds the subject. Its value is computed the first time it is called and kept for the rest of the expectation, and a `let` may call another. `with name: value` opens a context where the parameter has that value.
|
|
76
102
|
|
|
77
103
|
```ruby
|
|
78
|
-
|
|
79
|
-
on :upcase do
|
|
80
|
-
it MUST eq "HELLO"
|
|
81
|
-
end
|
|
104
|
+
require "fix"
|
|
82
105
|
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
Fix :Greeting do
|
|
107
|
+
let(:name) { "Bob" }
|
|
108
|
+
|
|
109
|
+
it MUST eq "Hello, Bob!"
|
|
110
|
+
|
|
111
|
+
with name: "Alice" do
|
|
112
|
+
it MUST eq "Hello, Alice!"
|
|
85
113
|
end
|
|
86
114
|
end
|
|
115
|
+
|
|
116
|
+
exit(Fix[:Greeting].test { "Hello, #{name}!" })
|
|
87
117
|
```
|
|
88
118
|
|
|
89
|
-
|
|
119
|
+
```text
|
|
120
|
+
example.rb:6: Success: expected "Hello, Bob!" to eq "Hello, Bob!"
|
|
121
|
+
example.rb:9: Success: expected "Hello, Alice!" to eq "Hello, Alice!"
|
|
122
|
+
```
|
|
90
123
|
|
|
91
|
-
|
|
124
|
+
The block of `test` runs on a fresh object for every expectation, where the lets of its context are methods: it sees them, and sees `Kernel` and the methods of the top level as any code does, but not the instance variables of the file. The value of a `with` is the object given, shared by the expectations of its context: `with items: []` gives the same array to each of them, where `let(:items) { [] }` gives a new one to each.
|
|
92
125
|
|
|
93
|
-
|
|
94
|
-
- **SHOULD/SHOULD_NOT**: Recommended practices with valid exceptions
|
|
95
|
-
- **MAY**: Optional features
|
|
126
|
+
A let is visible from the subject and from the other lets only. Named in an expectation, where it would be read before any subject exists, it raises `NameError` at load, with the remedy: `name is a let, visible in the subject block and in other lets only`. Its name cannot be a method of the object the subject is built on, public or private, nor a word of the language, so that nothing is redefined in silence: `let(:format)`, `let(:p)` or `let(:it)` raise `ArgumentError` at load, naming the owner (`format is already a method of Kernel`). A `with` has the same rule, and declares the lets it names: a name no `let` declared is a new parameter, not a mistake. A context may declare a name once, inherits the lets declared above it in the contexts around it, and may give a new value to one of them.
|
|
96
127
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
it SHOULD be_optimized # Recommended
|
|
101
|
-
it MAY include_metadata # Optional
|
|
102
|
-
end
|
|
103
|
-
```
|
|
128
|
+
### `it`
|
|
129
|
+
|
|
130
|
+
`it` takes a requirement, not a block: a level, then a matcher, as in `it MUST eq 42`. The levels are `MUST`, `MUST_NOT`, `SHOULD`, `SHOULD_NOT` and `MAY`, the words of RFC 2119, which has no negative MAY. The matchers are those of Matchi:
|
|
104
131
|
|
|
105
|
-
|
|
132
|
+
| Word | Meets when the actual value… |
|
|
133
|
+
|------|------------------------------|
|
|
134
|
+
| `eq(expected)` | is `eql?` to the expected one |
|
|
135
|
+
| `be(expected)` | is the very same object (`equal?`) |
|
|
136
|
+
| `be_true`, `be_false`, `be_nil` | is `true`, `false`, `nil` |
|
|
137
|
+
| `be_within(delta).of(expected)` | is a number within the delta |
|
|
138
|
+
| `match(regexp)` | is a String or a Symbol that matches |
|
|
139
|
+
| `be_an_instance_of(klass)` | is an instance of that very class |
|
|
140
|
+
| `be_a_kind_of(mod)` | is an instance of that module or of a descendant |
|
|
141
|
+
| `raise_exception(klass, message = nil)` | is an exception of that class or of a subclass, with that message (a String, or a Regexp to match) when given |
|
|
142
|
+
| `change(object, :method)`, `.by(n)`, `.by_at_least(n)`, `.by_at_most(n)`, `.from(a).to(b)`, `.to(b)` | makes that method of that object change, so when refined |
|
|
143
|
+
| `satisfy { \|value\| … }` | makes the block answer `true`; an answer that is neither `true` nor `false` is an error |
|
|
144
|
+
| `fix(:Name)`, `fix { … }` | meets that specification, by name or written here |
|
|
106
145
|
|
|
107
|
-
|
|
146
|
+
Any other object with `match?` and `to_s` is a matcher too. The object of a `change` must exist when the file is loaded, since the matcher is built there: a let, which lives with the subject, cannot be observed. A specification is a matcher that tests its expectations against the actual value, so one specification may require another:
|
|
108
147
|
|
|
109
148
|
```ruby
|
|
110
|
-
# first_test.rb
|
|
111
149
|
require "fix"
|
|
112
150
|
|
|
113
|
-
Fix :
|
|
114
|
-
|
|
151
|
+
Fix :Positive do
|
|
152
|
+
on :positive? do
|
|
153
|
+
it MUST be_true
|
|
154
|
+
end
|
|
115
155
|
end
|
|
116
156
|
|
|
117
|
-
Fix
|
|
118
|
-
|
|
157
|
+
Fix :Score do
|
|
158
|
+
it MUST be_a_kind_of Numeric
|
|
159
|
+
it MUST fix(:Positive)
|
|
160
|
+
end
|
|
119
161
|
|
|
120
|
-
|
|
162
|
+
exit(Fix[:Score].test { 7 })
|
|
163
|
+
```
|
|
121
164
|
|
|
122
|
-
```
|
|
123
|
-
|
|
165
|
+
```text
|
|
166
|
+
example.rb:10: Success: expected 7 to be a kind of Numeric
|
|
167
|
+
example.rb:11: Success: expected 7 to fix Positive
|
|
124
168
|
```
|
|
125
169
|
|
|
126
|
-
|
|
170
|
+
A failed `fix` says that the specification is not met, not which of its expectations failed: test the specification on its own for that.
|
|
127
171
|
|
|
128
|
-
|
|
172
|
+
In Ruby 3.4 and later, a bare `it` in a block is the parameter of the block, while `it` with a requirement is the word of Fix, as before; on Ruby 3.3 the bare `it` is the word of Fix too. A `satisfy` meant for both names its parameter: `satisfy { |value| … }`.
|
|
129
173
|
|
|
130
|
-
|
|
174
|
+
The document runs on an instance of `Fix::Dsl`, and a gem may add a word to it that composes `on` and `it`: fix-its adds `its`. Such a word gives `it` the `location:` of its own caller, what `caller_locations(1, 1).first` gives, an object with `path` and `lineno`, so that the result points at the line of the specification.
|
|
131
175
|
|
|
132
|
-
|
|
176
|
+
### Loading
|
|
133
177
|
|
|
134
|
-
|
|
135
|
-
Fix :UserAccount do
|
|
136
|
-
# Define reusable properties
|
|
137
|
-
let(:admin) { User.new(role: "admin") }
|
|
138
|
-
let(:guest) { User.new(role: "guest") }
|
|
178
|
+
The specification is built when its block runs, before any subject exists, and nothing in it waits for a test: a name that is not a Symbol, a name already registered, a `let`, a `with` or an `on` without a block, a `with` without a value, an `it` with a block or without a requirement, a matcher without `match?` or whose `to_s` is not a `String`, raise then. A block that raises registers nothing. `Fix[:Name]` raises `KeyError` for a name it does not know, and `fix(:Name)` finds its specification then: write it first.
|
|
139
179
|
|
|
140
|
-
|
|
141
|
-
it MUST be_an_instance_of User
|
|
180
|
+
`Fix.spec(:Name) { … }` is the method `Fix` calls; without a name, both give a specification that is not registered. The registry is written by the thread that loads the file, read afterwards, and never emptied: a runner runs each file in a process of its own.
|
|
142
181
|
|
|
143
|
-
|
|
144
|
-
with role: "admin" do
|
|
145
|
-
it MUST be_admin
|
|
182
|
+
## Outcomes
|
|
146
183
|
|
|
147
|
-
|
|
148
|
-
it MUST be_true
|
|
149
|
-
end
|
|
150
|
-
end
|
|
184
|
+
Two questions tell the levels apart. Does the method exist? MUST and SHOULD require it, MAY does not. Does it do what is specified? MUST and MAY require it, SHOULD recommends it. Hence five outcomes:
|
|
151
185
|
|
|
152
|
-
|
|
153
|
-
|
|
186
|
+
| Level | Match is true | Match is false | Missing method | Other exception |
|
|
187
|
+
|--------|---------------|----------------|----------------|-----------------|
|
|
188
|
+
| MUST | success | failure | error | error |
|
|
189
|
+
| SHOULD | success | warning | error | error |
|
|
190
|
+
| MAY | success | failure | info | error |
|
|
154
191
|
|
|
155
|
-
|
|
156
|
-
it MUST be_false
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
# Test specific methods
|
|
161
|
-
on :full_name do
|
|
162
|
-
with first_name: "John", last_name: "Doe" do
|
|
163
|
-
it MUST eq "John Doe"
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
on :update_password, "new_password" do
|
|
168
|
-
it MUST change(admin, :password_hash)
|
|
169
|
-
it MUST be_true # Return value check
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
This example demonstrates:
|
|
175
|
-
- Using `let` to define test fixtures
|
|
176
|
-
- Context-specific testing with `with`
|
|
177
|
-
- Method behavior testing with `on`
|
|
178
|
-
- Different requirement levels with `MUST`/`MUST_NOT`
|
|
179
|
-
- Testing state changes with the `change` matcher
|
|
180
|
-
- Nested contexts for complex scenarios
|
|
181
|
-
|
|
182
|
-
### Example 2: Duck Specification
|
|
183
|
-
|
|
184
|
-
Here's how Fix can be used to specify a Duck class:
|
|
192
|
+
A negation swaps the first two columns. A success, a warning and an info pass; a failure and an error do not. The missing method is the one named by the `on` of the context, when sending it raises `NoMethodError` for that method on that object, as the duck has no `speak`; Spectus adds the method a matcher calls on the actual value. A private method counts as missing, as does a method that exists and raises `NoMethodError` for itself; a method that raises `NoMethodError` on another object is an error.
|
|
185
193
|
|
|
186
194
|
```ruby
|
|
187
|
-
|
|
188
|
-
|
|
195
|
+
# account_fix.rb
|
|
196
|
+
require "fix"
|
|
189
197
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
198
|
+
class Account
|
|
199
|
+
def initialize(balance)
|
|
200
|
+
@balance = balance
|
|
193
201
|
end
|
|
194
202
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
203
|
+
attr_reader :balance
|
|
204
|
+
|
|
205
|
+
def withdraw(amount)
|
|
206
|
+
raise ArgumentError, "insufficient funds" if amount > balance
|
|
198
207
|
|
|
199
|
-
|
|
200
|
-
it MAY eql "♪... ♫..."
|
|
208
|
+
@balance -= amount
|
|
201
209
|
end
|
|
202
210
|
end
|
|
203
|
-
```
|
|
204
211
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
"Klop klop!"
|
|
212
|
+
Fix :Account do
|
|
213
|
+
on :balance do
|
|
214
|
+
it MUST eq 100
|
|
215
|
+
it MUST eq 200
|
|
216
|
+
it SHOULD eq 200
|
|
211
217
|
end
|
|
212
218
|
|
|
213
|
-
|
|
214
|
-
"
|
|
219
|
+
on :currency do
|
|
220
|
+
it MAY eq "EUR"
|
|
215
221
|
end
|
|
216
222
|
|
|
217
|
-
|
|
218
|
-
|
|
223
|
+
on :withdraw, 500 do
|
|
224
|
+
it MUST eq(-400)
|
|
219
225
|
end
|
|
220
226
|
end
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
Running the test:
|
|
224
227
|
|
|
225
|
-
|
|
226
|
-
Fix[:Duck].test { Duck.new }
|
|
228
|
+
exit(Fix[:Account].test { Account.new(100) })
|
|
227
229
|
```
|
|
228
230
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
- `be_an_instance_of(class)` - Verifies exact class match
|
|
239
|
-
- `be_a_kind_of(class)` - Checks class inheritance and module inclusion
|
|
240
|
-
|
|
241
|
-
### Change Testing Matchers
|
|
242
|
-
- `change(object, method)` - Base matcher for state changes
|
|
243
|
-
- `.by(n)` - Expects exact change by n
|
|
244
|
-
- `.by_at_least(n)` - Expects minimum change by n
|
|
245
|
-
- `.by_at_most(n)` - Expects maximum change by n
|
|
246
|
-
- `.from(old).to(new)` - Expects change from old to new value
|
|
247
|
-
- `.to(new)` - Expects change to new value
|
|
248
|
-
|
|
249
|
-
### Numeric Matchers
|
|
250
|
-
- `be_within(delta).of(value)` - Tests if a value is within ±delta of expected value
|
|
251
|
-
|
|
252
|
-
### Pattern Matchers
|
|
253
|
-
- `match(regex)` - Tests string against regular expression pattern
|
|
254
|
-
- `satisfy { |value| ... }` - Custom matching with block
|
|
231
|
+
```text
|
|
232
|
+
account_fix.rb:20: Success: expected 100 to eq 100
|
|
233
|
+
account_fix.rb:21: Failure: expected 100 to eq 200
|
|
234
|
+
account_fix.rb:22: Warning: expected 100 to eq 200
|
|
235
|
+
account_fix.rb:26: Info: NoMethodError: undefined method 'currency' for an instance of Account
|
|
236
|
+
account_fix.rb:30: Error: ArgumentError: insufficient funds
|
|
237
|
+
account_fix.rb:12:in 'Account#withdraw'
|
|
238
|
+
account_fix.rb:34:in '<main>'
|
|
239
|
+
```
|
|
255
240
|
|
|
256
|
-
|
|
257
|
-
- `be_true` - Tests for true
|
|
258
|
-
- `be_false` - Tests for false
|
|
259
|
-
- `be_nil` - Tests for nil
|
|
241
|
+
## Results
|
|
260
242
|
|
|
261
|
-
|
|
262
|
-
- `raise_exception(class)` - Tests if code raises specified exception
|
|
243
|
+
`test` writes on `$stdout`, for each expectation, one line: its location in the file, the outcome, and the summary of Spectus, `expected ACTUAL to DESCRIPTION`, with `not` for a negation, or `CLASS: MESSAGE`. A line break in either is written `\n`; a backslash stays as it is, the line being for reading, not for reading back. Below an error come the frames of its backtrace that are not in Fix, Aw, Defi, Matchi or Spectus, indented by two spaces. Nothing else is written: no letter, no colour, no count. What the subject writes on the standard output comes out as it is, before the lines of its context, so a text left without a line break joins the first of them.
|
|
263
244
|
|
|
264
|
-
|
|
265
|
-
- `be_*` - Dynamically matches `object.*?` methods (e.g., `be_empty` calls `empty?`)
|
|
266
|
-
- `have_*` - Dynamically matches `object.has_*?` methods (e.g., `have_key` calls `has_key?`)
|
|
245
|
+
That grammar, `LOCATION: OUTCOME: SUMMARY` then indented continuation lines, is what a runner reads: the location up to the first `:LINE: OUTCOME: ` of the line, the outcome one of the five words. What the subject writes is beyond the runner's reach; the status of the process is not.
|
|
267
246
|
|
|
268
|
-
|
|
247
|
+
`run` gives the same results as data, an `Array` of `Fix::Result` in the order of the contexts, a context before those inside it, and of the expectations within a context:
|
|
269
248
|
|
|
270
249
|
```ruby
|
|
271
|
-
|
|
272
|
-
it MUST be_an_instance_of Calculator
|
|
250
|
+
require "fix"
|
|
273
251
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
it MUST be_within(0.001).of(5.0)
|
|
277
|
-
end
|
|
252
|
+
Fix :Size do
|
|
253
|
+
it MUST be_an_instance_of String
|
|
278
254
|
|
|
279
|
-
on :
|
|
280
|
-
it MUST
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
with numbers: [1, 2, 3] do
|
|
284
|
-
it MUST_NOT be_empty
|
|
285
|
-
it MUST satisfy { |result| result.all? { |n| n.positive? } }
|
|
255
|
+
on :size do
|
|
256
|
+
it MUST eq 3
|
|
286
257
|
end
|
|
287
258
|
end
|
|
259
|
+
|
|
260
|
+
results = Fix[:Size].run { "abcd" }
|
|
261
|
+
results.map(&:outcome) # => [:success, :failure]
|
|
262
|
+
results.map(&:passed?) # => [true, false]
|
|
263
|
+
result = results.last
|
|
264
|
+
result.location # => "example.rb:7"
|
|
265
|
+
result.chain # => ".size"
|
|
266
|
+
result.summary # => "expected 4 to eq 3"
|
|
267
|
+
result.to_s # => "example.rb:7: Failure: expected 4 to eq 3"
|
|
268
|
+
result.result.class # => Spectus::Result
|
|
269
|
+
Fix[:Size].match? { "abc" } # => true
|
|
288
270
|
```
|
|
289
271
|
|
|
290
|
-
|
|
272
|
+
`location` is the file and the line of the `it`, relative to the current directory when under it; `chain` is the methods sent to the subject, as written after a dot with the arguments by their `inspect`, empty for the subject itself; `result` is the `Spectus::Result`, with its `level`, `negate`, `description`, `actual`, `error_class`, `error_message` and `backtrace`, unfiltered. `passed?`, `outcome` and `summary` are those of the result. A result is plain data, frozen.
|
|
291
273
|
|
|
292
|
-
|
|
274
|
+
`match?` tests a value, as a matcher does: the block is called once, where `match?` is called, and its value is the subject of every expectation, so the contexts cannot build it anew with their lets: a specification meant for `fix` is written without `with`.
|
|
293
275
|
|
|
294
|
-
|
|
295
|
-
- **Semantic Precision**: Express requirements with different levels of necessity
|
|
296
|
-
- **Fast Execution**: Get quick feedback on specification compliance
|
|
297
|
-
- **Pure Specifications**: Write specification documents that focus on behavior, not implementation
|
|
298
|
-
- **Rich Matcher Library**: Comprehensive set of matchers for different testing needs
|
|
299
|
-
- **Modern Ruby**: Takes advantage of modern Ruby features and practices
|
|
276
|
+
## Processes
|
|
300
277
|
|
|
301
|
-
|
|
278
|
+
Each context with expectations runs in a child process, forked from the one that tests: a subject that leaves something behind cannot touch the next context, and what it raises, an `exit` included, is an error of its expectation alone. The contexts run one after the other, a context before those inside it, and `test` writes the results of a context when its child returns them.
|
|
302
279
|
|
|
303
|
-
|
|
280
|
+
Nothing is rescued around a child, and Fix never writes a result that did not come from an expectation. An `Interrupt`, or a signal the child could trap, is raised again in the parent and stops the run. So does `Aw::ChildError`, for a child that died, was killed or left by `exit!`, or that a `break`, a `return` or a `throw` in the subject block left without a result: the results of the other expectations of its context are lost with it. The children a `fix` matcher forks are inside a matcher, so what they raise is an error of that expectation. On a Ruby without `fork`, a context with expectations raises `NotImplementedError`.
|
|
304
281
|
|
|
305
|
-
|
|
282
|
+
Each child starts as a copy of the parent, with its memory, its signal handlers and its open files, and ends with `exit!`, which runs no `at_exit` handler. Hence a `Random` seeded in the file gives every context the same sequence, while Ruby reseeds `Kernel#rand` in each child; what a subject writes to an IO other than the standard output and error is lost unless it flushes; and what SimpleCov measures in a child is lost, unless the result is stored before `exit!`, as the tests of Fix do, or the run uses `fork: false`. A subject that forks ends its child with `exit!`, as test code does: a child that runs on into Fix instead, by an `exit` or by nothing at all, is ended by Fix without a result. A `Timeout` or a signal in the parent ends the child at once, but not the children of that child, those a `fix` matcher forks or a subject's own, which run on until their work ends. A specification that requires itself at run time, through `satisfy` say, forks without end, and at the process limit a fork waits for a free slot rather than failing. The cost of a context is one fork of the testing process, a few milliseconds.
|
|
306
283
|
|
|
307
|
-
|
|
308
|
-
- [Bluesky](https://bsky.app/profile/fixrb.dev) - Latest updates and discussions
|
|
309
|
-
- [Documentation](https://www.rubydoc.info/gems/fix) - Comprehensive guides and API reference
|
|
310
|
-
- [Source Code](https://github.com/fixrb/fix) - Contribute and report issues
|
|
311
|
-
- [asciinema](https://asciinema.org/~fix) - Watch practical examples in action
|
|
284
|
+
`test(fork: false) { … }` and `run(fork: false) { … }` run the contexts in the process itself, for a debugger: an `exit!` of the subject then ends the process, what one context leaves behind reaches the next, and a `fix` matcher still forks.
|
|
312
285
|
|
|
313
286
|
## Versioning
|
|
314
287
|
|
|
315
|
-
|
|
288
|
+
Fix follows [Semantic Versioning 2.0](https://semver.org/).
|
|
316
289
|
|
|
317
290
|
## License
|
|
318
291
|
|