Skip to main content

Matchers

Matchers are passed to expect(actual).to, expect(actual).not_to, or block expectations such as expect { action }.to.

expect(actual).to matcher
expect(actual).not_to matcher
expect { action }.to matcher

Built-in Matchers

eq(expected)

Passes when actual == expected:

expect(1 + 2).to eq(3)
expect("hello").not_to eq("goodbye")

include(expected)

Passes when actual.include?(expected) returns true:

expect([1, 2, 3]).to include(2)
expect("smartest").to include("test")

start_with(prefix, ...)

Passes when actual.start_with?(*prefixes) returns true. Multiple prefixes pass if any prefix matches:

expect("about:blank").to start_with("about:")
expect("https://cdn-b.test/app.js").to start_with("https://cdn-a.test", "https://cdn-b.test")

end_with(suffix, ...)

Passes when actual.end_with?(*suffixes) returns true. Multiple suffixes pass if any suffix matches:

expect("screenshot.png").to end_with(".png")
expect("archive.tar.gz").to end_with(".zip", ".gz")

be_a(class_or_module) / be_an(class_or_module)

Passes when actual.is_a?(class_or_module) returns true. Subclasses and module inclusion are recognized:

expect("smartest").to be_a(String)
expect(StandardError.new("bad")).to be_an(Exception)

be_nil

Passes when actual.nil? is true:

expect(nil).to be_nil
expect("value").not_to be_nil

match(regexp)

Passes when regexp.match?(actual) returns true:

expect("https://example.test").to match(%r{\Ahttps://})
expect("about:blank").not_to match(%r{\Ahttps://})

contain_exactly(item, ...)

Passes when actual contains exactly the expected items, in any order. Duplicate expected items require duplicate actual items:

expect(%w[request close request]).to contain_exactly(
"request",
"request",
"close"
)

Expected items can be matcher objects, so contain_exactly can compose with other built-in or custom matchers:

expect(["request: /users", 200]).to contain_exactly(
match(%r{\Arequest: /users}),
eq(200)
)

match_array(items)

Equivalent to contain_exactly, but accepts the expected items as one array:

expect(%i[request close open]).to match_array(%i[open request close])

raise_error(error_class) / raise_error(message_regexp) / raise_error(error_class, message_regexp)

Passes when the block raises the expected error class, or when the raised error message matches the expected regexp. Pass both an error class and a message regexp to check both:

expect { Integer("x") }.to raise_error(ArgumentError)
expect { raise "request timed out" }.to raise_error(/timed out/)
expect { Integer("x") }.to raise_error(ArgumentError, /invalid/)

raise_error supports an error class, a message regexp, or both. No-argument and exact string message forms are not supported.

Fatal process-level exceptions such as SystemExit and Interrupt are re-raised instead of being treated as assertion failures.

change { value }

Passes when the value block returns a different value before and after the action block runs:

count = 0

expect { count += 1 }.to change { count }
expect { count += 1 }.to change { count }.by(1)
expect { count += 1 }.to change { count }.from(2).to(3)
expect { count }.not_to change { count }

from(expected), to(expected), and by(delta) can be chained together to constrain the before value, after value, and numeric difference.

change is only supported with block expectations and must receive a block. Smartest does not support RSpec's object-and-method form such as change(object, :method).

Generated Predicate Matcher

smartest --init creates smartest/matchers/predicate_matcher.rb and registers it from around_suite in smartest/test_helper.rb with use_matcher PredicateMatcher.

When enabled, be_<predicate> passes if actual.<predicate>? returns true:

expect([]).to be_empty
expect("value").not_to be_empty

Custom predicate methods work the same way:

expect(user).to be_active # calls user.active?

Arguments are forwarded to the predicate method:

expect(2).to be_between(1, 3) # calls 2.between?(1, 3)

The predicate matcher is generated as a normal custom matcher module, so projects that do not want this metaprogramming hook can remove the file and the use_matcher PredicateMatcher line from the generated around_suite block.

Custom Matchers

Define matcher methods in a module under smartest/matchers/.

Matcher methods should return an object that responds to:

  • matches?(actual)
  • failure_message
  • negated_failure_message
smartest/matchers/have_status_matcher.rb
module HaveStatusMatcher
class MatcherImpl
def initialize(expected)
@expected = expected
end

def matches?(actual)
@actual = actual
actual.status == @expected
end

def failure_message
"expected #{@actual.inspect} to have status #{@expected.inspect}"
end

def negated_failure_message
"expected #{@actual.inspect} not to have status #{@expected.inspect}"
end
end

def have_status(expected)
MatcherImpl.new(expected)
end
end

The generated smartest/test_helper.rb loads every Ruby file under smartest/matchers/ in sorted order. Register the matcher modules you want to use from around_suite with use_matcher:

smartest/test_helper.rb
require "smartest/autorun"

Dir[File.join(__dir__, "fixtures", "**", "*.rb")].sort.each do |fixture_file|
require fixture_file
end

Dir[File.join(__dir__, "matchers", "**", "*.rb")].sort.each do |matcher_file|
require matcher_file
end

around_suite do |suite|
use_matcher PredicateMatcher
use_matcher HaveStatusMatcher
suite.run
end

Registered matcher methods are available in every test that requires the helper:

Response = Struct.new(:status)

test("response status") do
expect(Response.new(200)).to have_status(200)
end