Matchi

Matchi is a lightweight, framework-agnostic Ruby library that provides a comprehensive set of expectation matchers for elegant and secure testing. Its design focuses on simplicity, security, and extensibility.

Key Features
- Framework Agnostic: Easily integrate with any Ruby testing framework
- Security-Focused Design: Built with robust type checking for most matchers
- Simple Integration: Minimal setup required to get started
- Extensible: Create custom matchers with just a few lines of code
- Comprehensive: Rich set of built-in matchers for common testing scenarios
- Well Documented: Extensive documentation with clear examples and implementation details
- Thread Safe: Immutable matchers design ensures thread safety in concurrent environments
Security Considerations for Predicate Matchers
While most Matchi matchers are designed to resist type spoofing, predicate matchers (Matchi::Predicate) rely on Ruby's dynamic method dispatch system and can be vulnerable to method overriding:
matcher = Matchi::Predicate.new(:be_empty)
array = []
def array.empty?
false
end
matcher.match? { array }
This limitation is inherent to Ruby's dynamic nature when working with predicate methods. If your tests require strict security guarantees, consider using direct state verification matchers instead of predicate matchers.
What is a Matchi Matcher?
A Matchi matcher is a simple Ruby object that follows a specific contract:
Here's the simplest possible matcher:
module Matchi
class SimpleEqual
def initialize(expected)
@expected = expected
end
def match?
raise ArgumentError, "a block must be provided" unless block_given?
@expected == yield
end
def to_s
"equal #{@expected.inspect}"
end
end
end
matcher = Matchi::SimpleEqual.new(42)
matcher.match? { 42 }
matcher.match? { "42" }
matcher.to_s
This design provides several benefits:
- Lazy Evaluation: The actual value is only computed when needed via the block
- Encapsulation: Each matcher is a self-contained object with clear responsibilities
- Composability: Matchers can be easily combined and reused
- Testability: The contract is simple and easy to verify
Installation
Add to your Gemfile:
gem "matchi"
Or install directly:
gem install matchi
Quick Start
require "matchi"
Matchi::Eq.new("hello").match? { "hello" }
Matchi::BeAKindOf.new(Numeric).match? { 42 }
Matchi::BeAKindOf.new(String).match? { 42 }
array = []
Matchi::Change.new(array, :length).by(2).match? { array.push(1, 2) }
Core Matchers
Value Comparison
Matchi::Eq.new("test").match? { "test" }
Matchi::Eq.new([1, 2, 3]).match? { [1, 2, 3] }
symbol = :test
Matchi::Be.new(symbol).match? { symbol }
string = "test"
Matchi::Be.new(string).match? { string.dup }
Type Checking
Matchi::BeAKindOf.new(Numeric).match? { 42.0 }
Matchi::BeAKindOf.new(Integer).match? { 42.0 }
Matchi::BeAnInstanceOf.new(Float).match? { 42.0 }
Matchi::BeAnInstanceOf.new(Numeric).match? { 42.0 }
Matchi::BeAKindOf.new("Numeric").match? { 42.0 }
Matchi::BeAnInstanceOf.new("Float").match? { 42.0 }
State Changes
counter = 0
Matchi::Change.new(counter, :to_i).by(5).match? { counter += 5 }
Matchi::Change.new(counter, :to_i).by_at_least(2).match? { counter += 3 }
Matchi::Change.new(counter, :to_i).by_at_most(5).match? { counter += 3 }
string = "hello"
Matchi::Change.new(string, :to_s).from("hello").to("HELLO").match? { string.upcase! }
array = []
Matchi::Change.new(array, :length).match? { array << 1 }
counter = 0
Matchi::Change.new(counter, :to_i).to(5).match? { counter = 5 }
Pattern Matching
Matchi::Match.new(/^test/).match? { "test_string" }
Matchi::Match.new(/^\d{3}-\d{2}$/).match? { "123-45" }
Matchi::Satisfy.new { |x| x.positive? && x < 10 }.match? { 5 }
Matchi::Satisfy.new { |arr| arr.all?(&:even?) }.match? { [2, 4, 6] }
Matchi::Predicate.new(:be_empty).match? { [] }
Matchi::Predicate.new(:have_key, :name).match? { { name: "Alice" } }
Exception Handling
Matchi::RaiseException.new(ArgumentError).match? { raise ArgumentError }
Matchi::RaiseException.new(StandardError).match? { raise ArgumentError }
Matchi::RaiseException.new("ArgumentError").match? { raise ArgumentError }
Numeric Comparisons
Matchi::BeWithin.new(0.5).of(3.0).match? { 3.2 }
Matchi::BeWithin.new(2).of(10).match? { 9 }
Creating Custom Matchers
Creating custom matchers is straightforward:
module Matchi
class BePositive
def match?
yield.positive?
end
def to_s
"be positive"
end
end
end
matcher = Matchi::BePositive.new
matcher.match? { 42 }
matcher.match? { -1 }
Security 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:
expected_value.eql?(actual_value)
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:
class MaliciousString
def eql?(other)
true
end
def ==(other)
true
end
end
actual = MaliciousString.new
expected = "expected string"
actual.eql?(expected)
expected.eql?(actual)
This is why Matchi's built-in matchers are implemented with this security consideration in mind. For example, the Eq matcher:
def match?
@expected.eql?(yield)
end
Extensions
matchi-fix
The matchi-fix gem extends Matchi with support for testing against Fix specifications. It provides a seamless integration between Matchi's matcher interface and Fix's powerful specification system.
gem "matchi-fix"
This extension adds a Fix matcher that allows you to verify implementation conformance to Fix test specifications across different testing frameworks like Minitest and RSpec.
Versioning
Matchi follows Semantic Versioning 2.0.
License
The gem is available as open source under the terms of the MIT License.
This project is sponsored by SashitĂŠ