Fixrb

Fix guide, version 1

Questions

What comes up with a first specification, in short answers.

The guide teaches each of these in turn; this page is for finding one by its keyword, with the short answer and the page that has the long one. Every answer was checked against Fix 1.0.1, whose README is itself run by its test suite.

Writing

Why does it MUST eq 42 fail for a subject that returns 42.0?

eq is eql?: the same value and the same class, for every value, not only numbers. 1.eql?(1.0) is false, and so is "a".eql?(:a). Compare with be_within(delta).of(42) for a number near 42, whatever its class, or with be_a_kind_of Numeric for the class alone. Matchers has each one.

Why can’t I write it MUST eq name when name is a let?

A let is a parameter of the subject, visible from the block of test and from the other lets only. Named in an expectation, it would be read while the file loads, before any subject exists, so Fix raises NameError at load: name is a let, visible in the subject block and in other lets only. Write the value in the expectation, and let the subject block read the let: with name: "Alice" do it MUST eq "Hello, Alice!" end, against test { "Hello, #{name}!" }. See let and with.

let(:format) raises ArgumentError. Why?

A let becomes a method of the object the subject block runs on, and that object sees Kernel and the methods of the top level. A let named like one of them, public or private (format, p, test, open, select, method, class, hash…), or like a word of the language (let, with, on, it, and its with fix-its), or like a matcher or a level (eq, match, change, fix, MUST), would redefine it in silence. The message names the owner or the word: format is already a method of Kernel. Pick another name.

What is the difference between let(:items) { [] } and with items: []?

A let is a block, called anew for each expectation: every expectation gets a new array. A with is a value, the very object given, shared by the expectations of its context: with items: [] gives them all the same array. Two contexts are two child processes, so the sharing never crosses a context.

Can a with name a let that was never declared?

Yes. A with declares the lets it names; a key no let declared is a new parameter, not an error. So a misspelt key is a new let, in silence: check the spelling against the subject block.

How do I test a method that takes a block?

on sends its method without a block, since the do … end of on is the context. Either call the method in the subject block, test { list.map { |x| x * 2 } }, or judge the subject with satisfy { |value| value.map { … } == … }.

Is there a be_empty, a have_key, a be_valid?

Not as words of Fix. The idiom is on(:empty?) { it MUST be_true }: the method is sent by on, and the line reads expected true to be true; run keeps the method in the chain of the result, .empty?. Any object with match? and to_s is a matcher, though, so it MUST Matchi::Predicate.new(:be_empty) works, with the matcher of matchi written in full. See Your own matcher.

Can change observe a let?

No. The object of change(object, :method) must exist when the file loads, since the matcher is built then, and a let lives with the subject, later: naming one there raises the NameError of a let read in an expectation. Observe an object of the file, a constant say.

it MUST fix(:Other) raises KeyError.

fix(:Name) finds its specification when the document is read, so Fix :Other must come earlier in the file, or in a file required earlier. The message is no specification named :Other; when a registered name is near, :Positive for a fix(:Positiv), Ruby adds Did you mean?  :Positive below it. See Composing specifications.

How do I test a class method, or a constructor?

Return the class from the subject block, and send the method with on: Fix[:Calculator].test { Calculator } with on :new, 2 do … end. An on inside that one is sent to the new instance.

On Ruby 3.4, its(:size) { MUST eq it } builds eq nil.

In Ruby 3.4 and later, a bare it in a block is the implicit parameter of the block; it with a requirement is still the word of Fix. On 3.3, the bare it is the word of Fix, and raises for want of a requirement. Write the value. A satisfy meant for both Rubies names its parameter: satisfy { |value| … }. The word its has the detail.

Outcomes

My raise_exception ArgumentError is a Failure, not an Error. The subject raised TypeError.

The exception is what raise_exception judges: another class is a match that is false, so a failure under MUST and MAY, a warning under SHOULD. An error is an exception that reached a matcher not made to judge one: with eq, the same TypeError is an error.

Why is a missing method an Info under MAY, but an Error under MUST?

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. A duck that does not speak meets on :speak do it MAY … end. The table of the five outcomes is in Writing specifications.

Does a warning make the run fail?

No. A success, a warning and an info pass; a failure and an error do not. test returns true for a run with warnings, and the status of the process is 0.

I see undefined method 'speak' on Ruby 3.4, but undefined method `speak' on 3.3.

Ruby changed the quote in 3.4, and a frame of a backtrace now names the class of its method. The texts are those of Ruby; Fix does not rewrite them.

Running

Why exit(Fix[:Duck].test { Duck.new }), with parentheses?

Without them, exit Fix[:Duck].test do … end gives the do … end block to exit, not to test, and test raises ArgumentError: a block must be given. Keep the parentheses, or use braces.

A file with two specifications: how does it exit?

Test them all, then exit once: exit([Fix[:A].test { … }, Fix[:B].test { … }].all?).

Nothing failed, but the process exits 1. Something failed, but it exits 0.

Look at the last line of the file: it passes the result of test to exit, and nothing else. test never calls exit, so a file that forgets it exits 0 whatever happened, and the command fix reads that status and nothing else. See Running specifications.

How do I run one file? All of them?

ruby fix/duck_fix.rb runs one, as any Ruby file. fix, from the fix-command gem, runs the *_fix.rb files of fix/, each in a new process, or the files and directories given: fix fix/duck_fix.rb fix/models.

My subject needs lib/ on the load path.

The command passes its environment to every file: RUBYOPT=-Ilib fix, or bundle exec fix for the gems of a Gemfile. bundle exec puts the gems on the load path; it does not require them, the file does. See Loading the code.

Does fix run files in parallel? Does it have a timeout?

No and no, in 1.0. Files run one after the other, each in its own process; Ctrl-C ends the command and, through the terminal, the file being run.

Is the order random?

Within a file, no: the contexts run in the order written, a context before those inside it, every time. Between files, on request: fix --random shuffles the files, and writes first the command that runs them in the same order again, fix --random=2. See Random order.

Where does the subject’s own output go?

It comes out as it is, on the same standard output, before the lines of its context. A text left without a line break joins the first of them. A runner reads the exit status, not the lines, for that reason.

SimpleCov reports nothing for the code my subjects ran.

Each context runs in a child that ends with exit!, which runs no at_exit handler, so SimpleCov never writes the child’s result. Either store the result before the child ends, as the tests of Fix do, or run with test(fork: false) for the measurement. See Debugging.

Does Fix work on Windows? On JRuby?

No. Fix forks a child per context and needs fork(2): a context with expectations raises NotImplementedError on a Ruby without it. Linux and macOS are tested in CI, on Ruby 3.3, 3.4, 4.0 and head.

Beside RSpec and minitest

Do I have to replace my test suite?

No. Fix needs nothing from another framework and changes nothing in it: a fix directory beside spec or test, run by its own command, and a Rakefile that runs them all. Fix holds the behaviour the pieces agree on, an interface, a contract with a dependency; the suite keeps its fixtures, its doubles and its framework. Next to other tests says how, and Coming from RSpec what each idiom becomes.

Can I check a specification of Fix from RSpec or minitest?

As a matcher of matchi: Matchi::Fix.new(:Duck).match? { Duck.new }, from the matchi-fix gem, answers true or false, forking the contexts as Fix does. In RSpec, expect(Matchi::Fix.new(:Duck).match? { duck }).to be true; in minitest, assert Matchi::Fix.new(:Duck).match? { duck }. The message of a failure then says false, no more: Fix[:Duck].test { duck } gives the lines. A specification meant for a matcher is written without with, since the value is given once and the contexts cannot build it anew.

A failed fix(:Other) does not say which expectation failed.

By design: a matcher answers true or false. Test the inner specification on its own, Fix[:Other].test { … }, for the lines.

Can I get the results as data instead of lines?

Fix[:Name].run { … } returns an Array of Fix::Result, frozen data with location, chain, outcome, passed?, summary, and the Spectus::Result underneath. test is run plus the printing. See Results as data.

How do I add a word to the language, as fix-its does?

Add a method to Fix::Dsl that composes on and it, and pass it the location: of your own caller, caller_locations(1, 1).first, so that the result points at the line of the specification. fix-its, nineteen lines of code, is the reference.

How does Fix compare with RSpec and minitest?

They do different jobs, and a project may have two of them. Where they differ:

FixRSpecminitest
The subjectchosen by the test, apart from the specification, which names no classsubject, in the spec; shared examples for several classesbuilt by the test class; a module included in several classes
LevelsMUST, SHOULD and MAY; a warning and an info passone, with pending and skipone, with skip
Isolationa child process per contextone process; hooks clean upone process; setup and teardown clean up
Hooksnone: the subject block runs for each expectationbefore, after, aroundsetup, teardown
Doublesnone, by designrspec-mocksminitest/mock
Orderthe order written; files shuffled on requestdefined; random on requestrandom by default
Outputone line per expectation, and the status of the processformattersreporters
Runs onCRuby with fork, tested on Linux and macOSevery Rubyevery Ruby