Matchers
What each one compares, and what it writes when it is met and when it is not.
A matcher is the second half of a requirement, after the level: it MUST eq 100. The matchers of Fix are those of Matchi, where the expected value makes the comparison wherever it can, so that the object under test cannot vouch for itself. Each one works with every level, and a negation, MUST_NOT or SHOULD_NOT, writes not in the line.
| Matcher | Met when the actual value… |
|---|---|
eq(expected) | is of its class and eql? to it |
be(expected) | is the very same object |
be_true, be_false, be_nil | is true, false, nil |
be_a_kind_of(mod) | is an instance of that module or of a descendant |
be_an_instance_of(klass) | is an instance of that very class |
be_within(d).of(x) | is a number at most d away from x |
match(regexp) | is a String or a Symbol that matches |
satisfy { |value| … } | makes the block answer true |
raise_exception(klass) | is an exception of that class, raised by the method |
change(object, :method) | comes back from a method that changed what object.method returns |
fix(:Name), fix { … } | meets that specification |
eq
eq(expected) is met when the actual value is of the class of the expected one, and eql? to it. Strings, symbols, arrays, hashes and data objects compare their content, element by element; a number compares its class too, so 1 is not eq 1.0.
require "fix"
Fix :Half do
on :to_s do
it MUST eq "1/2"
end
on :to_f do
it MUST eq 0.5
end
on :numerator do
it MUST eq 1.0
end
end
exit(Fix[:Half].test { Rational(1, 2) })
ruby eq_fix.rb eq_fix.rb:5: Success: expected "1/2" to eq "1/2" eq_fix.rb:9: Success: expected 0.5 to eq 0.5 eq_fix.rb:13: Failure: expected 1 to eq 1.0
The numerator is 1, an Integer, and 1 is not eql? to 1.0, a Float, although 1 == 1.0. Write the number of the class you expect, or be_within for a computed Float.
be
be(expected) is met when the actual value is the expected object itself, equal?: the object of a constant, a symbol, true, false, nil, or an object kept in a variable of the file.
require "fix"
DEFAULTS = { color: :blue }.freeze
Settings = Data.define(:defaults)
Fix :Settings do
on :defaults do
it MUST be DEFAULTS
it MUST eq({ color: :blue })
it MUST be({ color: :blue })
end
end
exit(Fix[:Settings].test { Settings.new(DEFAULTS) })
ruby be_fix.rb
be_fix.rb:9: Success: expected {color: :blue} to be {color: :blue}
be_fix.rb:10: Success: expected {color: :blue} to eq {color: :blue}
be_fix.rb:11: Failure: expected {color: :blue} to be {color: :blue}
The last line reads like the first, but fails: the hash written there has the same content as DEFAULTS, which eq accepts, and is another object.
be_true, be_false, be_nil
Each is met by one object only: true, false or nil. A value that Ruby treats as true in a condition, such as 4, is not true:
require "fix"
Fix :Word do
on :empty? do
it MUST be_false
end
on :index, "z" do
it MUST be_nil
end
on :size do
it MUST be_true
end
end
exit(Fix[:Word].test { "word" })
ruby be_true_fix.rb be_true_fix.rb:5: Success: expected false to be false be_true_fix.rb:9: Success: expected nil to be nil be_true_fix.rb:13: Failure: expected 4 to be true
To check that a collection is empty, ask it: on(:empty?) { it MUST be_true }. To accept any value that is neither nil nor false, write satisfy { |value| !!value }.
be_a_kind_of
be_a_kind_of(mod) is met by an instance of that class, of one of its subclasses, or of a class that includes that module:
require "fix"
Fix :Count do
it MUST be_a_kind_of Integer
it MUST be_a_kind_of Numeric
it MUST be_a_kind_of Comparable
it MUST be_a_kind_of String
end
exit(Fix[:Count].test { 42 })
ruby be_a_kind_of_fix.rb be_a_kind_of_fix.rb:4: Success: expected 42 to be a kind of Integer be_a_kind_of_fix.rb:5: Success: expected 42 to be a kind of Numeric be_a_kind_of_fix.rb:6: Success: expected 42 to be a kind of Comparable be_a_kind_of_fix.rb:7: Failure: expected 42 to be a kind of String
be_an_instance_of
be_an_instance_of(klass) is met by an instance of that very class only, not of a subclass:
require "fix"
Fix :Count do
it MUST be_an_instance_of Integer
it MUST be_an_instance_of Numeric
end
exit(Fix[:Count].test { 42 })
ruby be_an_instance_of_fix.rb be_an_instance_of_fix.rb:4: Success: expected 42 to be an instance of Integer be_an_instance_of_fix.rb:5: Failure: expected 42 to be an instance of Numeric
be_within
be_within(delta).of(expected) is met by a number, an Integer, a Float, a Rational or a BigDecimal, at most delta away from the expected one; a value of another class, a String say, does not meet it. It is the matcher for a computed Float, which is rarely exact:
require "fix"
Circle = Data.define(:radius) do
def area = Math::PI * radius**2
end
Fix :Circle do
on :area do
it MUST be_within(0.01).of(3.14)
it MUST be_within(0.001).of(3.14)
end
end
exit(Fix[:Circle].test { Circle.new(radius: 1) })
ruby be_within_fix.rb be_within_fix.rb:9: Success: expected 3.141592653589793 to be within 0.01 of 3.14 be_within_fix.rb:10: Failure: expected 3.141592653589793 to be within 0.001 of 3.14
match
match(regexp) is met by a String or a Symbol that the regular expression matches, and by nothing else:
require "fix"
Fix :Email do
it MUST match(/@example\.com\z/)
it MUST_NOT match(/\s/)
it MUST match(/\.org\z/)
on :to_sym do
it MUST match(/\Aada@/)
end
end
exit(Fix[:Email].test { "ada@example.com" })
ruby match_fix.rb match_fix.rb:4: Success: expected "ada@example.com" to match /@example\.com\z/ match_fix.rb:5: Success: expected "ada@example.com" not to match /\s/ match_fix.rb:6: Failure: expected "ada@example.com" to match /\.org\z/ match_fix.rb:9: Success: expected :"ada@example.com" to match /\Aada@/
satisfy
satisfy { |value| … } is met when its block answers true for the actual value, and not when it answers false. Any other answer is an error, so that a block that forgot to answer a question cannot pass:
require "fix"
Fix :Score do
it MUST satisfy { |score| score.between?(0, 100) }
it MUST satisfy { |score| score.even? }
it MUST satisfy { |score| score.to_s }
end
exit(Fix[:Score].test { 75 })
ruby satisfy_fix.rb satisfy_fix.rb:4: Success: expected 75 to satisfy &block satisfy_fix.rb:5: Failure: expected 75 to satisfy &block satisfy_fix.rb:6: Error: TypeError: the block must return true or false (got a String) satisfy_fix.rb:9:in '<main>'
Name the parameter of the block. Since Ruby 3.4, a bare it in a block is its parameter, while on Ruby 3.3 it is the word of Fix.
raise_exception
raise_exception(klass) is met when the method of the context raised an exception of that class or of a subclass: a KeyError is an IndexError. raise_exception(klass, message) also wants that message, a String, or a Regexp that matches it. A method that returns does not meet it, nor does another exception. Under any other matcher, the exception is an error: the it MUST eq 0 below gets one, with the frames of its backtrace that are not in Fix and its gems.
require "fix"
Fix :Stock do
on :fetch, :pears do
it MUST raise_exception KeyError
it MUST raise_exception KeyError, "key not found: :pears"
it MUST raise_exception IndexError
it MUST raise_exception ArgumentError
it MUST eq 0
end
on :fetch, :apples do
it MUST raise_exception KeyError
end
end
exit(Fix[:Stock].test { { apples: 3 } })
ruby raise_exception_fix.rb raise_exception_fix.rb:5: Success: expected #<KeyError: key not found: :pears> to raise exception KeyError raise_exception_fix.rb:6: Success: expected #<KeyError: key not found: :pears> to raise exception KeyError with message "key not found: :pears" raise_exception_fix.rb:7: Success: expected #<KeyError: key not found: :pears> to raise exception IndexError raise_exception_fix.rb:8: Failure: expected #<KeyError: key not found: :pears> to raise exception ArgumentError raise_exception_fix.rb:9: Error: KeyError: key not found: :pears raise_exception_fix.rb:17:in '<main>' raise_exception_fix.rb:13: Failure: expected 3 to raise exception KeyError
change
change(object, :method) is met when sending the method of the context makes that method of that object return another value. .by(n), .by_at_least(n) and .by_at_most(n) say by how much, .to(b) and .from(a).to(b) between which values. The line shows what the method of the context returned: here, click returns the new count.
require "fix"
COUNTER = Struct.new(:count).new(0)
Clicker = Data.define(:counter) do
def click = counter.count += 1
def look = counter.count
end
Fix :Clicker do
on :click do
it MUST change(COUNTER, :count).by(1)
end
on :click do
it MUST change(COUNTER, :count).from(0).to(1)
end
on :look do
it MUST change(COUNTER, :count).by_at_least(1)
end
end
exit(Fix[:Clicker].test { Clicker.new(COUNTER) })
ruby change_fix.rb change_fix.rb:12: Success: expected 1 to change by 1 change_fix.rb:16: Success: expected 1 to change from 0 to 1 change_fix.rb:20: Failure: expected 0 to change by at least 1
The matcher is built when the file is loaded, so the object it watches exists then: a constant, as here, not a let, which lives with the subject. The expectations of a context run in the same process, one after the other: a second change in the same context would see what the first did. Above, each expectation has a context of its own, which starts from the state of the file: the count is 0 again when look is sent, and look changes nothing.
fix
fix(:Name) is met when the actual value meets the specification of that name, written before it in the file or in a file it requires. fix { … } writes the specification in place, without a name:
require "fix"
Fix :Filled do
on :empty? do
it MUST be_false
end
end
Fix :Name do
it MUST be_an_instance_of String
it MUST fix(:Filled)
it MUST fix { on(:strip) { it MUST eq "Ada" } }
end
exit(Fix[:Name].test { "Ada" })
ruby fix_fix.rb fix_fix.rb:10: Success: expected "Ada" to be an instance of String fix_fix.rb:11: Success: expected "Ada" to fix Filled fix_fix.rb:12: Success: expected "Ada" to fix an anonymous specification
A specification in a specification: Composing specifications tells more.
Your own matcher
Any object with two methods is a matcher: match?, which gets the actual value from its block and answers true or false, and to_s, a String, which the line writes after to:
require "fix"
class BeEven
def match? = yield.even?
def to_s = "be even"
end
Fix :Pair do
on :size do
it MUST BeEven.new
end
on :first do
it MUST BeEven.new
end
end
exit(Fix[:Pair].test { [1, 2] })
ruby own_fix.rb own_fix.rb:10: Success: expected 2 to be even own_fix.rb:14: Failure: expected 1 to be even
MUST_NOT BeEven.new works as well: the level reads the answer.