Coming from RSpec
The same ideas under other names, and a few left out on purpose.
RSpec and Fix both describe behaviour with nested contexts and expectations. Two things differ at the root. A specification of Fix names no class: the subject comes from the test, on the last line, so that the same specification can test another one. And a specification is a document, read once when the file is loaded: it holds no code that runs before or after an expectation.
Structure
| RSpec | Fix |
|---|---|
RSpec.describe Counter do … end | Fix :Counter do … end |
subject { Counter.new(start) } | the block of the test: Fix[:Counter].test { Counter.new(start) } |
describe "#count", then subject.count in each example | on :count do … end |
let(:start) { 0 } | let(:start) { 0 }, visible from the subject block and the other lets only |
context with let(:start) { 5 } | with start: 5 do … end |
before { subject.increment } | on :increment do … end, around the expectations it prepares |
it { is_expected.to eq(0) } | it MUST eq 0 |
is_expected.not_to | it MUST_NOT |
shared_examples, it_behaves_like | a specification of its own, required with fix(:Name), or tested against several subjects |
rspec, rspec spec/models | fix, fix fix/models |
rspec --order random | fix --random, for the order of files |
rspec --seed 1234 | fix --random=1234 |
An RSpec spec of a counter, with a subject, a before that increments it in one context and a let overridden in another, becomes:
require "fix"
class Counter
attr_reader :count
def initialize(start) = @count = start
def increment = tap { @count += 1 }
end
Fix :Counter do
let(:start) { 0 }
on :count do
it MUST eq 0
end
on :increment do
on :count do
it MUST eq 1
end
end
with start: 5 do
on :count do
it MUST eq 5
end
end
end
exit(Fix[:Counter].test { Counter.new(start) })
ruby counter_fix.rb counter_fix.rb:14: Success: expected 0 to eq 0 counter_fix.rb:19: Success: expected 1 to eq 1 counter_fix.rb:25: Success: expected 5 to eq 5
The before became a method of the chain: increment returns the counter, and count is sent to it. A method that needs some preparation beyond a chain gets it in the subject block, from the lets.
Matchers
| RSpec | Fix |
|---|---|
eq(x), eql(x) | eq(x), which wants the class of x and uses eql?, where the eq of RSpec uses ==: 1.0 is not eq 1 |
equal(x), be(x) | be(x) |
be(true), be(false), be_nil | be_true, be_false, be_nil |
be_truthy, be_falsey | satisfy { |value| !!value }, satisfy { |value| !value } |
be_a, be_kind_of | be_a_kind_of |
be_an_instance_of, be_instance_of | be_an_instance_of |
be_within(d).of(x) | be_within(d).of(x) |
match(regexp) | match(regexp), for a String or a Symbol |
satisfy { |v| … }, be >= 3 | satisfy { |v| … }, answering true or false |
expect { … }.to raise_error(K) | raise_exception(K), the method of the context being what raises |
expect { … }.to change { o.count }.by(1) | change(o, :count).by(1) |
be_empty, and other predicates | on(:empty?) { it MUST be_true } |
include(x), start_with(x) | on(:include?, x) { it MUST be_true }, on(:start_with?, x) { … } |
have_attributes(name: "Ada") | on(:name) { it MUST eq "Ada" }, or its(:name) { MUST eq "Ada" } |
match_array, contain_exactly | on(:sort) { it MUST eq [1, 2, 3] } |
Three levels instead of pending
RSpec skips an example, or marks it pending so that its failure does not fail the run. Fix says it in the specification itself: SHOULD for what is recommended, a warning when it is not met; MAY for what is optional, an info when the method does not exist. Both pass, and both are written in the lines, so nothing is hidden. Writing specifications has the five outcomes.
Left out on purpose
- Hooks,
before,after,around: the subject block builds what a subject needs, for each expectation, and each context runs in a process of its own, so there is nothing to clean up in memory. - Doubles and mocks,
double,allow(…).to receive: give the subject a real object, or a small object of the file that stands for a service, through a let. A specification says what a subject does, not which messages it sends. - Shared state between examples,
before(:all), instance variables: a let lives with one subject, and a context cannot reach another; only awithshares its value, within its context. - Tags and filters,
focus:fixtakes files and directories, and a file is a program that can be run alone. aggregate_failures: every expectation gets its line, met or not.
Both can live in one project, each in its directory: In a project tells how.