Riteway Ruby
The standard testing assertion style for AI Driven Development (AIDD) and software agents.
This is the Ruby port of the riteway JavaScript library.
Riteway is a testing assertion style and philosophy which leads to simple, readable, helpful unit tests for humans and AI agents.
It lets you write better, more readable tests with a fraction of the code that traditional assertion frameworks would use.
Riteway is the AI-native way to build a modern test suite. It pairs well with RSpec, Claude Code, Cursor Agent, and more.
- Readable
- Isolated/Integrated
- Thorough
- Explicit
Riteway forces you to write Readable, Isolated, and Explicit tests, because that's the only way you can use the API. It also makes it easier to be thorough by making test assertions so simple that you'll want to write more of them.
Why Riteway for AI Driven Development?
Riteway's structured approach makes it ideal for AIDD:
📖 Learn more: Better AI Driven Development with Test Driven Development
- Clear requirements: The given/should structure and 5-question framework help AI better understand exactly what to build
- Readable by design: Natural language descriptions make tests comprehensible to both humans and AI
- Simple API: Minimal surface area reduces AI confusion and hallucinations
- Token efficient: Concise syntax saves valuable context window space
The 5 Questions Every Test Must Answer
There are 5 questions every unit test must answer. Riteway forces you to answer them.
- What is the unit under test (module, function, class, whatever)?
- What should it do? (Prose description)
- What was the actual output?
- What was the expected output?
- How do you reproduce the failure?
Installing
Add to your Gemfile:
gem "riteway"Or install directly:
gem install ritewayThen require the adapter for your test framework. Use one or the other — not both.
RSpec — require in spec/spec_helper.rb:
require "riteway/rspec"Optional: filter Riteway internals from RSpec backtraces so failures point directly to your test code:
RSpec.configure do |config|
config.backtrace_exclusion_patterns << /lib\/riteway/
endMinitest — require in test/test_helper.rb:
require "minitest/autorun"
require "riteway/minitest"Minitest ships with Ruby's standard library — no extra gem needed.
Example Usage
require "riteway/rspec"
# A function to test
def sum(*args)
args.each { |n| raise TypeError, "Not a number: #{n.inspect}" unless n.is_a?(Numeric) }
args.reduce(0, :+)
end
RSpec.describe "sum()" do
it "given no arguments, should return 0" do
Riteway.assert(
given: "no arguments",
should: "return 0",
actual: sum(),
expected: 0
)
end
it "given zero, should return the correct sum" do
Riteway.assert(
given: "zero",
should: "return the correct sum",
actual: sum(2, 0),
expected: 2
)
end
it "given negative numbers, should return the correct sum" do
Riteway.assert(
given: "negative numbers",
should: "return the correct sum",
actual: sum(1, -4),
expected: -3
)
end
it "given a non-numeric argument, should raise TypeError" do
error = Riteway.attempt(method(:sum), 1, "NaN")
Riteway.assert(
given: "a non-numeric argument",
should: "raise TypeError",
actual: error.class,
expected: TypeError
)
end
endOutput
Riteway uses your test framework's output. The failure message always includes the given/should context; the diff format varies slightly between RSpec (colorized diff) and Minitest (simple expected/got lines).
For RSpec, use --format documentation for full prose output:
bundle exec rspec --format documentationsum()
given no arguments, should return 0
given zero, should return the correct sum
given negative numbers, should return the correct sum
given a non-numeric argument, should raise TypeError
Finished in 0.00112 seconds
4 examples, 0 failures
When a test fails, the given/should context is always included in the error:
Given negative numbers: should return the correct sum
expected: -3
got: 0
API
Riteway.assert
Riteway.assert(given:, should:, actual:, expected:) => void, raisesThe core assertion. Takes keyword arguments and compares actual to expected using deep equality (eq). All four arguments are required — missing any raises Ruby's native ArgumentError.
assert uses RSpec's eq matcher, which handles deep comparison of arrays, hashes, and nested structures.
Riteway.assert(
given: "an array of numbers",
should: "equal the expected array",
actual: [1, 2, 3].map { |n| n * 2 },
expected: [2, 4, 6]
)Riteway.attempt
Riteway.attempt(callable = nil, *args, **kwargs, &block) => Error | AnyExecute a callable or block with the given arguments. Returns the error if one is raised, otherwise returns the result. Designed for testing error cases in your assertions. Supports positional args, keyword args, lambdas, procs, and blocks.
attempt catches StandardError and its subclasses only. Exceptions outside this hierarchy — SystemExit, Interrupt, SignalException — propagate normally. Note that RSpec's ExpectationNotMetError inherits from Exception (not StandardError), so it propagates through attempt rather than being caught and returned.
# Block form (most concise)
error = Riteway.attempt { Integer("not a number") }
# Lambda form
error = Riteway.attempt(-> { Integer("not a number") })
# Method reference with positional args
error = Riteway.attempt(method(:sum), 1, "NaN")
# Method reference with keyword args
result = Riteway.attempt(method(:create_user), name: "Alice", age: 30)
Riteway.assert(
given: "a non-numeric string",
should: "raise ArgumentError",
actual: error.class,
expected: ArgumentError
)Riteway.count_keys
Riteway.count_keys(hash = {}) => IntegerGiven a hash, return a count of its keys. Defaults to {} (returns 0) when called with no arguments. A convenience wrapper for hash.keys.length that reads naturally inside a Riteway.assert call. Handy when you're adding new state to a hash keyed by ID and want to ensure the correct number of keys were added.
Riteway.assert(
given: "a hash with 3 keys",
should: "return 3",
actual: Riteway.count_keys({ a: 1, b: 2, c: 3 }),
expected: 3
)Riteway.match
Riteway.match(text) => ->(pattern) => StringTake some text to search and return a lambda which takes a pattern and returns the matched text, or nil if no match — consistent with Ruby's own String#match. The pattern can be a String or Regexp. String patterns are auto-escaped so regex meta-characters are treated as literals.
contains = Riteway.match("<h1>Dialog Title</h1>")
Riteway.assert(
given: "some text and a string pattern",
should: "return the matched text",
actual: contains.call("Dialog Title"),
expected: "Dialog Title"
)
Riteway.assert(
given: "some text and a regex pattern",
should: "return the matched text",
actual: contains.call(/\w+/),
expected: "Dialog"
)
Riteway.assert(
given: "a pattern that does not match",
should: "return nil",
actual: contains.call("not found"),
expected: nil
)You can also use contains.("pattern") or contains["pattern"] as shorthand for .call.
Publishing
See RELEASING.md for the full release process.
rake release is intentionally disabled to prevent automated publishing.
License
MIT