Written by James BrittTechnically reviewed by Jim Freeze · Reviewed
Choose the version of this Ruby programming test that fits where you are today: Newbie, Medium, Hard or Extra Hard. There are ten questions at each level. Work through them in practice mode for feedback as you go, or try an exam attempt before opening the answer key. A coding challenge follows for each level.
You can stay with one level or attempt all 40 questions together. Flag the ones you want to revisit. After submitting, use the result to pick a few concepts to practise, then retry the questions you missed or left unanswered.

How to use this Ruby programming test
Set aside around 8–15 minutes for ten questions, with more time for the longer Hard and Extra Hard snippets. None of the levels has a countdown. If you have only just started Ruby, choose Newbie and take your time reading each line. You do not need Rails knowledge.
Make a prediction before running any code. Then choose an answer. In practice mode, Check answer reveals the explanation and locks that choice for this attempt. Exam mode lets you revise your choices until you submit. Flagging a question simply marks it for your attention; it does not change its score.
Scoring is straightforward: one point for a correct answer and zero for an incorrect or unanswered one. A level is scored out of ten, while the mixed test is out of 40. The coding challenges have their own checks and stay outside that percentage. The browser saves your current quiz attempt when local storage is available; Reset this attempt clears it.
Choose your Ruby programming test level
Newbie: ten questions on truthiness, arithmetic, interpolation, ranges, basic array operations, simple Hash lookups, return values and nil. Start here if you have read a few Ruby snippets but still find yourself guessing their output.
Medium: ten questions on shared references, transformations, fetch defaults, keyword arguments, blocks, instance state, iteration, ensure and input conversion. This level suits someone who can write a short method and wants to catch everyday mistakes.
Hard: ten questions on shared Hash defaults, shallow freezing, String keys, Proc and lambda argument handling, prepend, rescue scope, false-valued memoisation and accumulators. Trace the objects and method calls carefully; familiar-looking code can behave differently from your first guess.
Extra Hard: ten questions on closure capture, nonlocal returns, lazy enumeration, regular-expression anchors, keyword separation, constant lookup, refinements, module order, pattern matching and identity-based Hashes. This is intended for readers already comfortable with Ruby methods and collections.
These levels describe the difficulty of this question bank. They are not certifications or standard job grades. The quiz does not assess Rails, database design, concurrency, application security or your ability to maintain a production system.
Start the interactive Ruby programming test
Choose an answer for each question, then open its explanation to check your reasoning. Each correct answer is worth one point. Choose a level and work through its ten questions.
Ruby programming test questions
Newbie Ruby programming test
Basics
Does zero count as true?
What does this program print?
puts(0 ? "runs" : "skips")
Show answer and explanation
Answer B. Zero takes the first branch in Ruby. The two falsey values are false and nil; everything else is truthy, including an empty String or Array. If you expected skips, check whether you brought a truth rule over from another language.
Basics
Integer and floating-point division
What does p display?
p [7 / 2, 7 / 2.0]
Show answer and explanation
Answer C. With two Integer operands, 7 / 2 gives 3. Change either operand to a Float and the result can contain a fractional part: 7 / 2.0 gives 3.5. The types going into the calculation matter.
Basics
String interpolation
Which two lines are printed, in order?
name = "Ada"
puts "Hello #{name}"
puts 'Hello #{name}'
Show answer and explanation
Answer A. The double-quoted greeting inserts Ada. The single-quoted greeting keeps #{name} as literal characters. If a message prints the placeholder you meant to replace, its quote style is a useful first thing to check.
Basics
An exclusive range
What is the resulting array?
p (1...4).to_a
Show answer and explanation
Answer D. The extra dot leaves 4 out, giving [1, 2, 3]. Writing 1..4 would include it. In both cases the range starts at 1; only the treatment of the end value changes.
Collections
Selecting matching values
Which array is printed?
p [1, 2, 3, 4].select { |n| n.odd? }
Show answer and explanation
Answer A. The odd numbers survive, so you get [1, 3]. select keeps an original element whenever its block result is truthy. To collect the true and false results themselves, you would use map.
Collections
What compact actually removes
Which elements survive?
p [nil, false, 0, ""].compact
Show answer and explanation
Answer D. Only nil disappears. false, 0 and the empty string all remain in the returned array. Cleaning blank text as well would need another condition; compact has a narrower job.
Collections
Symbol keys and string keys
What does this lookup return?
settings = { timeout: 5 }
p settings["timeout"]
Show answer and explanation
Answer B. timeout: creates a Symbol key, :timeout. Looking for the String key “timeout” misses it and returns nil. Rails users should take particular care here: an ordinary core Ruby Hash has no automatic indifferent access.
Methods
A method’s return value
What does p print?
def double(number)
number * 2
"done"
end
p double(4)
Show answer and explanation
Answer B. The multiplication produces 8, but the method goes on to evaluate “done”. That last expression becomes its return value. Move the multiplication to the end, or return it explicitly, if 8 is what the caller needs.
Debugging
Safe navigation stops at nil
What does this expression return?
user = nil
p user&.upcase
Show answer and explanation
Answer C. The receiver is nil, so &. skips upcase and returns nil. It only provides that shortcut for nil. A false receiver would still receive the method call, and exceptions from a method that actually runs can still escape.
Debugging
An out-of-range array index
What does the ordinary bracket lookup return?
colors = [:red, :blue]
p colors[5]
Show answer and explanation
Answer D. Index 5 is outside this two-element array, and the bracket lookup returns nil. If a missing position should fail loudly, Array#fetch can raise IndexError instead; it also supports an explicit default.
Medium Ruby programming test
Basics
Two references, one string
Assume the default mutable string behavior shown here. What prints?
# frozen_string_literal: false
a = "ruby"
b = a
b.upcase!
puts a
Show answer and explanation
Answer B. One mutable String is being referenced by two variables. Modifying it through b means that a also sees RUBY. The first line explicitly selects mutable string literals, so this snippet does not raise FrozenError.
Collections
Mapping without replacing the original array
What are the two output lines?
numbers = [1, 2, 3]
doubled = numbers.map { |n| n * 2 }
p numbers
p doubled
Show answer and explanation
Answer C. numbers remains [1, 2, 3], and doubled becomes [2, 4, 6]. map creates a new array from the values produced by its block. But a mutable object used inside that block, such as a String, could still be changed even while a new result array is being built.
Collections
A missing key versus a nil value
What does fetch return in these two cases?
limits = { max: nil }
p [limits.fetch(:max, 10), limits.fetch(:missing, 10)]
Show answer and explanation
Answer C. There are two scenarios. :max exists and its value is nil, so fetch returns that value. :missing does not exist at all; here fetch uses its second argument, 10. This distinction helps when an explicit nil means something in your data.
Methods
Passing a keyword argument
Which greeting is printed?
def label(name, prefix: "Hi")
"#{prefix}, #{name}"
end
puts label("Ada", prefix: "Hello")
Show answer and explanation
Answer D. Hello replaces the default Hi for this particular call, giving Hello, Ada. prefix: is the keyword argument, while Ada fills the positional name parameter. Follow the definition and the call together to see which argument supplies each value.
Methods
Passing a value to a block
What is the method’s result?
def with_value
yield 2
end
p with_value { |n| n * 3 }
Show answer and explanation
Answer A. Look at the value flowing through the call: yield passes 2 into the block, the block multiplies it by 3, and the method returns that product. The number printed is 6.
Methods
Separate instance state
What values do the two objects hold?
class Counter
attr_reader :value
def initialize
@value = 0
end
def increment
@value += 1
end
end
a = Counter.new
b = Counter.new
a.increment
p [a.value, b.value]
Show answer and explanation
Answer C. a has been incremented once; b has not. Their @value variables are therefore 1 and 0 respectively. The reader created by attr_reader lets us retrieve those values without making them shared class state.
Methods
each is not map
Which value is assigned to result?
result = [1, 2].each { |n| n * 10 }
p result
Show answer and explanation
Answer B. The products are generated and then discarded. When this Array#each call completes normally, it returns [1, 2], the array it traversed. Use map when you want a new array containing the products.
Debugging
Requiring a Hash key
Which message is printed?
settings = {}
begin
settings.fetch(:token)
rescue KeyError
puts "missing token"
end
Show answer and explanation
Answer D. The required :token key is missing. fetch raises KeyError, so the rescue prints missing token. Bracket access would return nil in its place. Whether that absence counts as an error is a decision for your application.
Debugging
ensure and the return value
Which two lines appear, in order?
def answer
7
ensure
puts "cleanup"
end
p answer
Show answer and explanation
Answer A. cleanup prints before the caller receives 7. ensure executes as the method ends, but its ordinary puts statement leaves the pending return value intact. An explicit return in ensure would change that behavior.
Debugging
A conversion with trailing text
Which pair is printed?
lenient = "12cats".to_i
begin
strict = Integer("12cats", 10)
rescue ArgumentError
strict = :invalid
end
p [lenient, strict]
Show answer and explanation
Answer B. to_i accepts the leading 12 and ignores the trailing cats. Integer with base 10 rejects those letters, so the rescue supplies :invalid. Before choosing a conversion for user-entered text, decide which behavior its input contract needs.
Hard Ruby programming test
Collections
A shared Hash default
What does this Hash contain after the append?
groups = Hash.new([])
groups[:a] << 1
p [groups[:b], groups.keys]
Show answer and explanation
Answer C. No entries have been added to the Hash. Missing keys return the same default array, which now contains 1. One useful fix for separate per-key containers is a default block that assigns a fresh array to the missing key.
Collections
Freezing a container
What happens when the nested string is changed?
# frozen_string_literal: false
items = ["a"]
items.freeze
items.first << "b"
p items
Show answer and explanation
Answer B. Although the array is frozen, the String inside it remains mutable. Appending b changes the String to ab without replacing an element or adding a new element to the array. Freezing that outer container does not recursively freeze its contents.
Collections
A String used as an ordinary Hash key
Which lookups succeed after the original string changes?
# frozen_string_literal: false
key = "a"
lookup = { key => 1 }
key.upcase!
p [lookup["a"], lookup["A"]]
Show answer and explanation
Answer D. The stored key remains a. An ordinary Hash duplicates and freezes an unfrozen String key, so modifying the original String later has no effect on that stored copy. String keys receive special treatment; other mutable key objects are not guaranteed the same behavior.
Methods
Lambda argument counts
Which branch runs?
add = ->(a, b) { a + b }
begin
add.call(1)
rescue ArgumentError
puts "wrong argument count"
end
Show answer and explanation
Answer A. The lambda expects two arguments and receives only one. ArgumentError is raised before a + b can be evaluated, and the rescue prints wrong argument count. Compare its argument handling with the ordinary Proc in the following question.
Methods
A non-lambda Proc’s missing argument
What does this simple Proc receive?
pair = proc { |a, b| [a, b] }
p pair.call(1)
Show answer and explanation
Answer B. The missing second positional argument becomes nil, producing [1, nil]. This lenient behavior belongs to the non-lambda Proc with ordinary block parameters shown here. Keep keyword arguments and more complex destructuring arrangements separate when reasoning about argument rules.
Methods
prepend and super
Which name is printed?
module Tagged
def name
"tag:" + super
end
end
class Report
prepend Tagged
def name
"report"
end
end
puts Report.new.name
Show answer and explanation
Answer C. Tagged runs before Report because it was prepended. Its super reaches Report#name, which supplies report. Tagged#name then adds the prefix, leaving tag:report as the returned name.
Debugging
Choosing the rescue scope
Which rescue prints a message?
begin
begin
raise NotImplementedError
rescue StandardError
puts "inner"
end
rescue NotImplementedError
puts "outer"
end
Show answer and explanation
Answer D. The outer rescue catches the exception. NotImplementedError lies outside the StandardError branch and therefore passes the inner rescue. A bare rescue defaults to StandardError too; it would not catch every Exception type.
Debugging
Memoising a false result
How many times is the calculation performed?
class Probe
attr_reader :calls
def initialize
@calls = 0
end
def ready?
@ready ||= begin
@calls += 1
false
end
end
end
probe = Probe.new
2.times { probe.ready? }
p probe.calls
Show answer and explanation
Answer B. The calculation is performed twice. Its first result is false, which causes the right-hand side of ||= to run again on the second call to ready?. To memoise a result that may be false or nil, record separately whether the calculation has happened.
Debugging
An explicit return in ensure
Which return wins?
def value
return 1
ensure
return 2
end
p value
Show answer and explanation
Answer A. The later explicit return takes effect, so value returns 2 to its caller. A return inside ensure can replace a pending result and can also suppress a pending exception. Keep cleanup clauses free of explicit returns unless you deliberately need that behavior.
Collections
Rebinding an immutable accumulator
What does each_with_object return here?
result = [1, 2, 3].each_with_object(0) do |number, total|
total += number
end
p result
Show answer and explanation
Answer C. The result is still 0. total += number creates another Integer and rebinds the block’s local variable to it. The original accumulator remains unchanged. Consider reduce when the value from each iteration should become the accumulator for the next one.
Extra Hard Ruby programming test
Methods
Closures over one changing variable
Which values do the saved lambdas return?
tasks = []
i = 0
while i < 3
tasks << -> { i }
i += 1
end
p tasks.map(&:call)
Show answer and explanation
Answer D. All three calls read i after the loop, when it is 3. The lambdas share that captured variable rather than storing a snapshot of its earlier value. No per-iteration captured value was created here.
Methods
A Proc returning after its method has finished
What happens when the saved Proc is called?
def build_handler
proc { return :done }
end
handler = build_handler
begin
p handler.call
rescue LocalJumpError
puts "no active method"
end
Show answer and explanation
Answer C. build_handler has already returned when handler.call runs. The Proc’s return tries to leave that finished method invocation and raises LocalJumpError, which the rescue handles. A return inside a lambda would leave the lambda itself.
Collections
Stopping a lazy pipeline
How many source values are processed?
seen = 0
values = (1..).lazy.map do |number|
seen += 1
number * 2
end.take(3).force
p [values, seen]
Show answer and explanation
Answer A. Only three source values pass through the mapping block. Lazy evaluation lets take(3) request just enough work before force collects [2, 4, 6]. An eager map over the endless range would have no finishing point.
Debugging
String anchors versus line anchors
How do the two expressions handle the trailing newline?
text = "12\n"
p [/\A[0-9]+\z/.match?(text), /^[0-9]+$/.match?(text)]
Show answer and explanation
Answer B. The newline makes the whole-string pattern fail. The line-based pattern can still match the digits before that newline, so its result is true. For validation of an entire input string, use absolute string anchors rather than line boundaries.
Methods
A positional Hash is not a keyword argument
Under Ruby 3.2’s argument rules, which output appears?
def quantity(limit:)
limit
end
options = { limit: 3 }
begin
p quantity(options)
rescue ArgumentError
puts "use keyword expansion"
end
Show answer and explanation
Answer D. options is passed as a positional Hash, while quantity expects a keyword. Ruby 3.2 raises ArgumentError for this call. quantity(**options) is the keyword expansion that would pass limit: 3 correctly.
Methods
Qualified class syntax and constant lookup
Which LABEL does this method find?
module Outer
LABEL = "outer"
end
class Parent
LABEL = "parent"
end
class Outer::Child < Parent
def label
LABEL
end
end
puts Outer::Child.new.label
Show answer and explanation
Answer B. The inherited LABEL in Parent is found, giving parent. Writing class Outer::Child does not establish the same lexical nesting as opening module Outer and defining Child inside it. A class’s qualified name alone does not describe its constant lookup context.
Methods
Refinements and the call site
Which pair of greetings is printed?
class Greeter
def greeting
"hello"
end
end
module Loud
refine Greeter do
def greeting
"HELLO"
end
end
end
def earlier(object)
object.greeting
end
using Loud
greeter = Greeter.new
p [greeter.greeting, earlier(greeter)]
Show answer and explanation
Answer C. The direct call uses the refinement and says HELLO. earlier was defined before using Loud, so its call retains the earlier refinement context and says hello. Sending the same object into a method does not carry the caller’s active refinements with it.
Methods
Two included modules and super
What is the method lookup order in the result?
module First
def chain
["first"] + super
end
end
module Second
def chain
["second"] + super
end
end
class Base
def chain
["base"]
end
end
class Child < Base
include First
include Second
end
p Child.new.chain
Show answer and explanation
Answer A. Second was included last, so it contributes first. Its super reaches First, whose super reaches Base. The resulting array records that order as second, first, base.
Debugging
Exact keys in a Hash pattern
Which branch matches this record?
record = { id: 7, extra: true }
case record
in { id: Integer => id, **nil }
puts "exact keys"
in { id: Integer => id }
puts "contains #{id}"
end
Show answer and explanation
Answer D. The extra key prevents the first pattern from matching: **nil forbids unlisted keys. The second pattern allows extras, requires an Integer :id and captures 7. That branch prints contains 7.
Collections
Identity-based Hash keys
What happens to equal but distinct String objects?
# frozen_string_literal: false
a = "ruby"
b = "ruby"
lookup = {}.compare_by_identity
lookup[a] = 1
lookup[b] = 2
p [lookup.size, lookup[a], lookup["ruby"]]
Show answer and explanation
Answer B. There are two entries because a and b are distinct objects. This identity-based Hash finds the original a, but the third String created during lookup is a different key and returns nil. Equal text is not enough when key identity is the comparison rule.
How to read your Ruby programming test results
A result is most useful when it tells you what to do next. Look through the missed answers and find one you can explain now. If the explanation still feels mysterious, copy its snippet into a small Ruby file and trace it one line at a time. The topic breakdown helps you spot where several mistakes have a common cause.
For this question bank, use 80–100% as a sign that most tested ideas are familiar, 50–79% as a prompt for focused practice and below 50% as a reason to revisit the foundations of that level. These are informal study bands. Compare Newbie attempts with Newbie and Extra Hard attempts with Extra Hard: the material is different, and a small topic score is only a limited sample.
Retrying missed and unanswered questions starts a fresh practice attempt with a smaller set. A higher score can show that an explanation helped, but you have also seen those questions before. Return to the full level later if you want a more useful comparison with your first attempt.
Turn a missed answer into a useful experiment
Choose a small example you misunderstood and change one thing. Swap map for each, replace a String key with a Symbol key, or remove the default from fetch. Write down your prediction before you run it. Comparing that prediction with the output gives you a specific mistake to investigate.
For additional exercises, work through our practical Ruby code examples. If transformations and blocks need more attention, the Ruby functional programming guide connects those ideas in longer examples.
Use the official Ruby Array reference to check indexing, map, select and compact. The official Ruby Hash reference explains key lookup and fetch. Both links point to Ruby 3.4 documentation, making the reference version explicit.
Four Ruby coding challenges: one for each level
For the coding challenges, start with the contract: what input is allowed, what output is required and what must stay unchanged. Draft your method in the scratchpad, then copy it into a local Ruby file with the supplied checks. The scratchpad neither executes Ruby nor saves your work. A worked solution is there when you want to compare approaches.
Keep going after the supplied example works. An empty array, repeated value or malformed argument can expose an assumption you missed. Use the checks as a starting point, add a case of your own and explain which error that case would catch.
Newbie challenge: Keep the unique positive integers
Given an Array of Integers, return a new array containing only positive values, without duplicates, in their original order. Do not modify the input. Assume the input already meets the stated type contract.
def unique_positive(numbers)
# Return a new array.
end
Checks to run with your method
input = [-2, 3, 0, 3, 5, -1]
raise "values" unless unique_positive(input) == [3, 5]
raise "empty" unless unique_positive([]) == []
raise "order" unless unique_positive([5, 2, 5, 1]) == [5, 2, 1]
raise "input changed" unless input == [-2, 3, 0, 3, 5, -1]
puts "Challenge 1 checks passed"
Show worked solution
def unique_positive(numbers)
numbers.select { |n| n.positive? }.uniq
end
select expresses the positive-value rule, and uniq removes later repetitions while preserving the first occurrence. Both calls here return new arrays. The mutation check compares the original input after the method call.
Medium challenge: Count normalised labels
Given an Array of Strings, strip surrounding whitespace, convert each string to lowercase, skip empty results and count each remaining label in a Hash. Do not modify the input strings. Assume all input elements are Strings; these examples use ASCII text.
def label_counts(labels)
# Return a hash of normalised labels and counts.
end
Checks to run with your method
input = [" Ruby ", "ruby", "", "SWIFT", " "]
raise "counts" unless label_counts(input) == { "ruby" => 2, "swift" => 1 }
raise "empty" unless label_counts([]) == {}
raise "input changed" unless input.first == " Ruby "
raise "separate calls" unless label_counts(["Ruby"]) == { "ruby" => 1 }
puts "Challenge 2 checks passed"
Show worked solution
def label_counts(labels)
labels.each_with_object(Hash.new(0)) do |label, counts|
key = label.strip.downcase
counts[key] += 1 unless key.empty?
end
end
The accumulator belongs to this method call. Hash.new(0) supplies zero for an unseen label, while += writes the updated count. strip and downcase create the cleaned string here, leaving the caller’s original text intact.
Hard challenge: Validate a small quantity
Accept only a String containing one or two ASCII decimal digits whose numeric value is between 1 and 20 inclusive. Return the Integer value, or nil for anything else. Reject surrounding whitespace, signs and trailing text. Under this contract, “01” is accepted as 1.
def parse_quantity(value)
# Return an integer from 1 to 20, or nil.
end
Checks to run with your method
raise "lower bound" unless parse_quantity("1") == 1
raise "upper bound" unless parse_quantity("20") == 20
raise "leading zero" unless parse_quantity("01") == 1
["0", "21", " 2", "2 ", "2cats", "+2", "2\n", "001", "", nil, 2].each do |bad|
raise "accepted #{bad.inspect}" unless parse_quantity(bad).nil?
end
puts "Challenge 3 checks passed"
Show worked solution
def parse_quantity(value)
return nil unless value.is_a?(String)
return nil unless /\A[0-9]{1,2}\z/.match?(value)
number = Integer(value, 10)
(1..20).cover?(number) ? number : nil
end
The type check runs before the regular expression. The absolute string anchors require the entire string to contain just one or two ASCII digits, and the final range check enforces the business limit. Specifying base 10 keeps the leading-zero case unambiguous.
Extra Hard challenge: Stop a lazy event stream at the right time
Given an Enumerable of mixed objects and a nonnegative Integer limit, return the first limit distinct positive Integer IDs from Hash events whose :active value is exactly true. Ignore other events. Preserve encounter order and do not consume more source events after enough IDs have been collected. A zero limit must consume nothing; an invalid limit must raise ArgumentError.
def first_valid_ids(events, limit:)
# Filter lazily and stop once the requested IDs are found.
end
Checks to run with your method
seen = 0
source = [nil, { id: 2, active: true }, { id: 2, active: true },
{ id: 3, active: false }, { id: 4, active: true },
{ id: 9, active: true }].lazy.map { |event| seen += 1; event }
raise "IDs" unless first_valid_ids(source, limit: 2) == [2, 4]
raise "over-consumed" unless seen == 5
never = Enumerator.new { raise "must not consume" }
raise "zero" unless first_valid_ids(never, limit: 0) == []
raise "empty" unless first_valid_ids([], limit: 3) == []
raise "invalid event" unless first_valid_ids([{ id: "2", active: true },
{ id: 0, active: true }, { id: 3, active: 1 }], limit: 3) == []
begin
first_valid_ids([], limit: -1)
raise "negative limit accepted"
rescue ArgumentError
# Expected.
end
puts "Challenge 4 checks passed"
Show worked solution
def first_valid_ids(events, limit:)
unless limit.is_a?(Integer) && limit >= 0
raise ArgumentError, "limit must be a nonnegative Integer"
end
events.lazy.filter_map do |event|
next unless event.is_a?(Hash) && event[:active] == true
id = event[:id]
id if id.is_a?(Integer) && id.positive?
end.uniq.take(limit).force
end
The lazy chain validates events before collecting IDs. uniq remembers IDs already seen, and take limits how many unique IDs are requested. The counter check catches accidental eager evaluation or reading past the second unique valid ID. The zero-limit check uses an Enumerator that would raise if touched.
Ruby programming test: common questions
Is this Ruby programming test suitable for beginners?
Yes. Choose Newbie for ten questions on basic Ruby behavior and use practice mode to see feedback as you go. Medium, Hard and Extra Hard provide separate sets when you want a greater challenge.
Do I need to install Ruby to take the quiz?
No installation is needed for the multiple-choice quiz. To run your own solutions and the supplied challenge checks, use a local Ruby installation. The page scores selected answers; it does not execute arbitrary Ruby code.
Can I use the score to prepare for an interview?
Use it to identify topics to practise before an interview. Also write and explain programs, discuss trade-offs and debug unfamiliar code. A score on this short question bank cannot establish job readiness.
Can I retake the test?
Yes. Choose any level again, shuffle its question order, attempt all 40 questions or retry only missed and unanswered items. Your saved attempt stays in this browser, and the reset control clears it.
About this Ruby programming test
All 40 question snippets and the four worked challenge solutions were executed with Ruby 3.2.3. The linked references use Ruby 3.4. The test concerns core Ruby; individual questions state assumptions where they matter.
