Matchi
This library provides a comprehensive set of matchers for testing different aspects of your code. Each matcher is designed to handle specific verification needs while maintaining a clear and expressive syntax.
Project goals
- Adding matchers should be as simple as possible.
- Being framework agnostic and easy to integrate.
- Avoid false positives/negatives due to malicious actual values.
Installation
Add this line to your application's Gemfile:
gem "matchi"
And then execute:
bundle install
Or install it yourself as:
gem install matchi
Overview
Matchi provides a collection of damn simple expectation matchers.
Usage
To make Matchi available:
require "matchi"
All examples here assume that this has been done.
Anatomy of a matcher
A Matchi matcher is a simple Ruby object that follows these requirements:
It must implement a
match?
method that:- Accepts a block as its only parameter
- Executes that block to get the actual value
- Returns a boolean indicating if the actual value matches the expected criteria
Optionally, it may implement:
to_s
: Returns a human-readable description of the match criteria
Built-in matchers
Here is the collection of generic matchers.
Basic Comparison Matchers
Be
Checks for object identity using Ruby's equal?
method.
Matchi::Be.new(:foo).match? { :foo } # => true (same object)
Matchi::Be.new("test").match? { "test" } # => false (different objects)
Eq
Verifies object equivalence using Ruby's eql?
method.
Matchi::Eq.new("foo").match? { "foo" } # => true (equivalent content)
Matchi::Eq.new([1, 2]).match? { [1, 2] } # => true (equivalent arrays)
Type and Class Matchers
BeAnInstanceOf
Verifies exact class matching (no inheritance).
Matchi::BeAnInstanceOf.new(String).match? { "test" } # => true
Matchi::BeAnInstanceOf.new(Integer).match? { 42 } # => true
Matchi::BeAnInstanceOf.new(Numeric).match? { 42 } # => false (Integer, not Numeric)
BeAKindOf
Verifies class inheritance and module inclusion.
Matchi::BeAKindOf.new(Numeric).match? { 42 } # => true (Integer inherits from Numeric)
Matchi::BeAKindOf.new(Numeric).match? { 42.0 } # => true (Float inherits from Numeric)
Pattern Matchers
Match
Tests string patterns against regular expressions.
Matchi::Match.new(/^foo/).match? { "foobar" } # => true
Matchi::Match.new(/\d+/).match? { "abc123" } # => true
Matchi::Match.new(/^foo/).match? { "barfoo" } # => false
Satisfy
Provides custom matching through a block.
Matchi::Satisfy.new { |x| x.positive? && x < 10 }.match? { 5 } # => true
Matchi::Satisfy.new { |x| x.start_with?("test") }.match? { "test_file" } # => true
State Change Matchers
Change
Verifies state changes in objects with multiple variation methods:
Basic Change
array = []
Matchi::Change.new(array, :length).by(2).match? { array.push(1, 2) } # => true
Minimum Change
counter = 0
Matchi::Change.new(counter, :to_i).by_at_least(2).match? { counter += 3 } # => true
Maximum Change
value = 10
Matchi::Change.new(value, :to_i).by_at_most(5).match? { value += 3 } # => true
From-To Change
string = "hello"
Matchi::Change.new(string, :upcase).from("hello").to("HELLO").match? { string.upcase! } # => true
To-Only Change
number = 1
Matchi::Change.new(number, :to_i).to(5).match? { number = 5 } # => true
Numeric Matchers
BeWithin
Checks if a number is within a specified range of an expected value.
Matchi::BeWithin.new(0.5).of(3.0).match? { 3.2 } # => true
Matchi::BeWithin.new(5).of(100).match? { 98 } # => true
Behavior Matchers
RaiseException
Verifies that code raises specific exceptions.
Matchi::RaiseException.new(ArgumentError).match? { raise ArgumentError } # => true
Matchi::RaiseException.new(NameError).match? { undefined_variable } # => true
Predicate
Creates matchers for methods ending in ?
.
Using be_
prefix
Matchi::Predicate.new(:be_empty).match? { [] } # => true (calls empty?)
Matchi::Predicate.new(:be_nil).match? { nil } # => true (calls nil?)
Using have_
prefix
Matchi::Predicate.new(:have_key, :foo).match? { { foo: 42 } } # => true (calls has_key?)
Custom matchers
Custom matchers could easily be added to Matchi
module to express more specific expectations.
A Be the answer matcher:
module Matchi
class BeTheAnswer
def match?
expected.equal?(yield)
end
private
def expected
42
end
end
end
matcher = Matchi::BeTheAnswer.new
matcher.match? { 42 } # => true
A Be prime matcher:
require "prime"
module Matchi
class BePrime
def match?
Prime.prime?(yield)
end
end
end
matcher = Matchi::BePrime.new
matcher.match? { 42 } # => false
A Start with matcher:
module Matchi
class StartWith
def initialize(expected)
@expected = expected
end
def match?
/\A#{@expected}/.match?(yield)
end
end
end
matcher = Matchi::StartWith.new("foo")
matcher.match? { "foobar" } # => true
Best Practices
Proper Value Comparison Order
One of the most critical aspects when implementing matchers is the order of comparison between expected and actual values. Always compare values in this order:
# GOOD: Expected value controls the comparison
expected_value.eql?(actual_value)
# BAD: Actual value controls the comparison
actual_value.eql?(expected_value)
Why This Matters
The order is crucial because the object receiving the comparison method controls how the comparison is performed. When testing, the actual value might come from untrusted or malicious code that could override comparison methods:
# Example of how comparison can be compromised
class MaliciousString
def eql?(other)
true # Always returns true regardless of actual equality
end
def ==(other)
true # Always returns true regardless of actual equality
end
end
actual = MaliciousString.new
expected = "expected string"
actual.eql?(expected) # => true (incorrect result!)
expected.eql?(actual) # => false (correct result)
This is why Matchi's built-in matchers are implemented with this security consideration in mind. For example, the Eq
matcher:
# Implementation in Matchi::Eq
def match?
@expected.eql?(yield) # Expected value controls the comparison
end
Contact
- Home page: https://github.com/fixrb/matchi
- Bugs/issues: https://github.com/fixrb/matchi/issues
Versioning
Matchi follows Semantic Versioning 2.0.
License
The gem is available as open source under the terms of the MIT License.
Sponsors
This project is sponsored by Sashité