Fixrb

Fix guide, version 1

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.

MatcherMet 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_nilis 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.