Intro to RSpec and Test-Driven Development: A Visual, Hands-On Guide
Learn RSpec and test-driven development by building a factorial method one failing test at a time: red, green, refactor, reading failures, matchers, edge cases, real run-by-run results, speed measurements, and exercises with answers.

On this page
How do you know your code works? You run it, try a few inputs, and eyeball the answers. That works once. But every time you change the code you'd have to try all those inputs again, and you won't. Automated tests do the trying for you, in a fraction of a second, every time.
RSpec is the most popular testing tool for Ruby, and test-driven development (TDD) is a way of working where you write the test before the code. In this guide you'll build a factorial method the TDD way, one failing test at a time, and see every real message RSpec prints along the way. You'll learn to read failures, use matchers, handle edge cases, and keep your code safe while you clean it up. Exercises with answers wrap it up.
All the outputs below come from real runs with RSpec 3.13 on Ruby 3.3. Your timings ("Finished in…") will differ a little from run to run.
The test-driven development cycle

TDD has three steps, named after the colors test tools print:
- Red: write a small test for something the code should do. Run it and watch it fail (there's no code yet, after all).
- Green: write the simplest code that makes that test pass. Run all the tests.
- Refactor: clean up the code while keeping every test green.
Then repeat with the next small piece of behavior. The tests pile up into a safety net that checks everything you've built, every time you run them.
Our goal: factorial

The factorial of a number, written n!, multiplies the number by every whole number below it down to 1: 4! = 4 × 3 × 2 × 1 = 24. The base cases are 1! = 1 and, by definition, 0! = 1.
Notice how the table is already a list of tests: each row is an input with the answer we expect.
Setting up
Install RSpec (once per computer) and check the version:
1$ gem install rspec
2$ rspec --version
3RSpec 3.13
4 - rspec-core 3.13.6
5 - rspec-expectations 3.13.5
6 - rspec-mocks 3.13.8
7 - rspec-support 3.13.7Make a project folder, go into it, and let RSpec set itself up:
1$ mkdir factorial
2$ cd factorial
3$ rspec --init
4 create .rspec
5 create spec/spec_helper.rb
Then create an empty factorial.rb for the code and a spec/factorial_spec.rb for the tests. RSpec runs every file in spec/ whose name ends in _spec.rb, so those two naming rules matter.
Red: the first test
Put this in spec/factorial_spec.rb:
1require_relative "../factorial"
2
3RSpec.describe "factorial" do
4 it "returns 1 for 0" do
5 expect(factorial(0)).to eq(1)
6 end
7end
describenames the thing being tested and groups its examples.itis one example: a single behavior, described in words.expect(actual).to matcheris the check. Here the matchereq(1)means "equals 1".
Read it out loud: factorial returns 1 for 0: expect factorial of 0 to equal 1. That readability is RSpec's whole style, but there's no magic: describe, it, expect and eq are plain Ruby methods taking strings and blocks.
Run the tests from the project folder (the one that holds spec/):
1$ rspec
2F
3
4Failures:
5
6 1) factorial returns 1 for 0
7 Failure/Error: expect(factorial(0)).to eq(1)
8
9 NoMethodError:
10 undefined method `factorial' for #<RSpec::ExampleGroups::Factorial:0x000001f19d775700>
11 # ./spec/factorial_spec.rb:5:in `block (2 levels) in <top (required)>'
12
13Finished in 0.01337 seconds (files took 0.30762 seconds to load)
141 example, 1 failure
15
16Failed examples:
17
18rspec ./spec/factorial_spec.rb:4 # factorial returns 1 for 0Red, as expected: there's no factorial method yet. (The 0x0000… number is a memory address and changes every run.) Each F in the first line is a failing example; a passing one prints a dot.
Green, one tiny step at a time
In TDD you let the failure tell you what to do next, and you do only that. Define the method:
def factorial
end1$ rspec
2F
3
4Failures:
5
6 1) factorial returns 1 for 0
7 Failure/Error: expect(factorial(0)).to eq(1)
8
9 ArgumentError:
10 wrong number of arguments (given 1, expected 0)
11 # ./factorial.rb:1:in `factorial'
12 # ./spec/factorial_spec.rb:5:in `block (2 levels) in <top (required)>'
13
14Finished in 0.01436 seconds (files took 0.31405 seconds to load)
151 example, 1 failure
16
17Failed examples:
18
19rspec ./spec/factorial_spec.rb:4 # factorial returns 1 for 0A new failure is progress. The test passes one argument but the method takes none, so add a parameter:
def factorial(number)
end1$ rspec
2F
3
4Failures:
5
6 1) factorial returns 1 for 0
7 Failure/Error: expect(factorial(0)).to eq(1)
8
9 expected: 1
10 got: nil
11
12 (compared using ==)
13 # ./spec/factorial_spec.rb:5:in `block (2 levels) in <top (required)>'
14
15Finished in 0.04566 seconds (files took 0.29809 seconds to load)
161 example, 1 failure
17
18Failed examples:
19
20rspec ./spec/factorial_spec.rb:4 # factorial returns 1 for 0Now the method runs, but an empty method returns nil. What's the simplest code that returns 1? Returning 1:
def factorial(number)
1
end1$ rspec
2.
3
4Finished in 0.02012 seconds (files took 0.44638 seconds to load)
51 example, 0 failuresGreen! Returning a constant feels like cheating, and it is, on purpose: the code is only as smart as the tests demand. The next tests will force it to get smarter.
Here's the journey so far:
| Run | RSpec said | What it means | The fix |
|---|---|---|---|
| 1 | NoMethodError: undefined method 'factorial' | The method doesn't exist | Define it |
| 2 | ArgumentError: wrong number of arguments (given 1, expected 0) | It takes the wrong number of arguments | Add a parameter |
| 3 | expected: 1, got: nil | It runs but returns the wrong value | Return 1 |
| 4 | 1 example, 0 failures | Green | Write the next test |
How to read a failure

Nicer output: the documentation format
rspec --init created a .rspec file holding options RSpec uses every run. Add a second line so it prints each example by name:
--require spec_helper
--format documentationMore tests, smarter code
Add two more examples to the spec:
1require_relative "../factorial"
2
3RSpec.describe "factorial" do
4 it "returns 1 for 0" do
5 expect(factorial(0)).to eq(1)
6 end
7
8 it "returns 1 for 1" do
9 expect(factorial(1)).to eq(1)
10 end
11
12 it "returns 2 for 2" do
13 expect(factorial(2)).to eq(2)
14 end
15end1$ rspec
2
3factorial
4 returns 1 for 0
5 returns 1 for 1
6 returns 2 for 2 (FAILED - 1)
7
8Failures:
9
10 1) factorial returns 2 for 2
11 Failure/Error: expect(factorial(2)).to eq(2)
12
13 expected: 2
14 got: 1
15
16 (compared using ==)
17 # ./spec/factorial_spec.rb:13:in `block (2 levels) in <top (required)>'
18
19Finished in 0.0123 seconds (files took 0.40719 seconds to load)
203 examples, 1 failure
21
22Failed examples:
23
24rspec ./spec/factorial_spec.rb:12 # factorial returns 2 for 2The new test for 1 passed straight away (our constant happens to be right), and the one for 2 fails. The simplest fix? Still a little cheeky:
1def factorial(number)
2 return 1 if number <= 1
3
4 number
5end1$ rspec
2
3factorial
4 returns 1 for 0
5 returns 1 for 1
6 returns 2 for 2
7
8Finished in 0.00293 seconds (files took 0.37575 seconds to load)
93 examples, 0 failuresThis trick is called fake it till you make it. To force a real solution, add examples that no shortcut can satisfy. This is called triangulation:
1 it "returns 6 for 3" do
2 expect(factorial(3)).to eq(6)
3 end
4
5 it "returns 24 for 4" do
6 expect(factorial(4)).to eq(24)
7 end
8
9 it "returns 5040 for 7" do
10 expect(factorial(7)).to eq(5040)
11 end1$ rspec
2
3factorial
4 returns 1 for 0
5 returns 1 for 1
6 returns 2 for 2
7 returns 6 for 3 (FAILED - 1)
8 returns 24 for 4 (FAILED - 2)
9 returns 5040 for 7 (FAILED - 3)
10
11Failures:
12
13 1) factorial returns 6 for 3
14 Failure/Error: expect(factorial(3)).to eq(6)
15
16 expected: 6
17 got: 3
18
19 (compared using ==)
20 # ./spec/factorial_spec.rb:17:in `block (2 levels) in <top (required)>'
21
22 2) factorial returns 24 for 4
23 Failure/Error: expect(factorial(4)).to eq(24)
24
25 expected: 24
26 got: 4
27
28 (compared using ==)
29 # ./spec/factorial_spec.rb:21:in `block (2 levels) in <top (required)>'
30
31 3) factorial returns 5040 for 7
32 Failure/Error: expect(factorial(7)).to eq(5040)
33
34 expected: 5040
35 got: 7
36
37 (compared using ==)
38 # ./spec/factorial_spec.rb:25:in `block (2 levels) in <top (required)>'
39
40Finished in 0.01422 seconds (files took 0.33051 seconds to load)
416 examples, 3 failures
42
43Failed examples:
44
45rspec ./spec/factorial_spec.rb:16 # factorial returns 6 for 3
46rspec ./spec/factorial_spec.rb:20 # factorial returns 24 for 4
47rspec ./spec/factorial_spec.rb:24 # factorial returns 5040 for 7Look at the "got" values: 3, 4, 7. The method just hands back its input. Time to write it for real, multiplying from the number down to 2:
1def factorial(number)
2 product = 1
3 multiplier = number
4
5 while multiplier > 1
6 product *= multiplier
7 multiplier -= 1
8 end
9
10 product
11end1$ rspec
2
3factorial
4 returns 1 for 0
5 returns 1 for 1
6 returns 2 for 2
7 returns 6 for 3
8 returns 24 for 4
9 returns 5040 for 7
10
11Finished in 0.0032 seconds (files took 0.33583 seconds to load)
126 examples, 0 failuresAll green. The base cases are handled too: for 0 and 1 the loop never runs and product stays 1.
Refactor: clean up with a safety net
The while loop works, but Ruby can say "multiply 1 through n" in one line with reduce. Because the tests exist, we can try it without fear:
def factorial(number)
(1..number).reduce(1, :*)
end1$ rspec
2
3factorial
4 returns 1 for 0
5 returns 1 for 1
6 returns 2 for 2
7 returns 6 for 3
8 returns 24 for 4
9 returns 5040 for 7
10
11Finished in 0.00318 seconds (files took 0.32349 seconds to load)
126 examples, 0 failuresStill green, so the shorter version behaves exactly the same on everything we've specified. reduce(1, :*) starts at 1 and multiplies in each number of the range; for 0 the range 1..0 is empty, so the answer is the starting 1.
Edge cases: huge numbers and negative input
What else should factorial do? Two good questions to turn into tests:
- Big numbers. Factorials grow absurdly fast.
- Bad input. Factorial isn't defined for negative numbers.

1 it "handles numbers too big for 64 bits" do
2 expect(factorial(25)).to eq(15_511_210_043_330_985_984_000_000)
3 end
4
5 it "rejects negative numbers" do
6 expect { factorial(-1) }.to raise_error(ArgumentError)
7 endNotice the curly braces in the second test: expect { … } hands RSpec a block of code to run, so it can watch for an error. With parentheses, the error would happen before RSpec could catch it.
1$ rspec
2
3factorial
4 returns 1 for 0
5 returns 1 for 1
6 returns 2 for 2
7 returns 6 for 3
8 returns 24 for 4
9 returns 5040 for 7
10 handles numbers too big for 64 bits
11 rejects negative numbers (FAILED - 1)
12
13Failures:
14
15 1) factorial rejects negative numbers
16 Failure/Error: expect { factorial(-1) }.to raise_error(ArgumentError)
17 expected ArgumentError but nothing was raised
18 # ./spec/factorial_spec.rb:33:in `block (2 levels) in <top (required)>'
19
20Finished in 0.01434 seconds (files took 0.31673 seconds to load)
218 examples, 1 failure
22
23Failed examples:
24
25rspec ./spec/factorial_spec.rb:32 # factorial rejects negative numbersThe big-number test passed immediately: Ruby integers grow as large as needed. The negative test is red, because factorial(-1) quietly returns 1, a wrong answer that looks right. Raise an error instead:
1def factorial(number)
2 raise ArgumentError, "factorial is not defined for negative numbers" if number.negative?
3
4 (1..number).reduce(1, :*)
5end1$ rspec
2
3factorial
4 returns 1 for 0
5 returns 1 for 1
6 returns 2 for 2
7 returns 6 for 3
8 returns 24 for 4
9 returns 5040 for 7
10 handles numbers too big for 64 bits
11 rejects negative numbers
12
13Finished in 0.00767 seconds (files took 0.33693 seconds to load)
148 examples, 0 failuresRefactoring the tests: a table of cases
Tests are code too, and they deserve cleanup. Those six nearly identical examples can come from a hash of inputs and answers, and context groups the examples for one situation:
1require_relative "../factorial"
2
3RSpec.describe "factorial" do
4 {
5 0 => 1,
6 1 => 1,
7 2 => 2,
8 3 => 6,
9 4 => 24,
10 7 => 5040,
11 25 => 15_511_210_043_330_985_984_000_000,
12 }.each do |number, expected|
13 it "returns #{expected} for #{number}" do
14 expect(factorial(number)).to eq(expected)
15 end
16 end
17
18 context "with a negative number" do
19 it "raises an ArgumentError" do
20 expect { factorial(-1) }.to raise_error(ArgumentError, /negative/)
21 end
22 end
23end1$ rspec
2
3factorial
4 returns 1 for 0
5 returns 1 for 1
6 returns 2 for 2
7 returns 6 for 3
8 returns 24 for 4
9 returns 5040 for 7
10 returns 15511210043330985984000000 for 25
11 with a negative number
12 raises an ArgumentError
13
14Finished in 0.00831 seconds (files took 0.44918 seconds to load)
158 examples, 0 failuresSame eight checks, less repetition, and adding a case is now one line. The /negative/ also checks the error's message. (The hashes guide covers the each with two block parameters.)
The whole session at a glance

Every red bar is a test that failed before the code existed to pass it. That's the proof each test can actually catch a problem.
Tests catch a broken refactor
Here's why the safety net matters. Suppose someone "simplifies" the method, reasoning that multiplying by 1 is pointless:
1def factorial(number)
2 raise ArgumentError, "factorial is not defined for negative numbers" if number.negative?
3
4 (2..number).reduce(:*)
5endIt looks fine and works for 2, 3, 4… But:
1$ rspec
2
3factorial
4 returns 1 for 0 (FAILED - 1)
5 returns 1 for 1 (FAILED - 2)
6 returns 2 for 2
7 returns 6 for 3
8 returns 24 for 4
9 returns 5040 for 7
10 returns 15511210043330985984000000 for 25
11 with a negative number
12 raises an ArgumentError
13
14Failures:
15
16 1) factorial returns 1 for 0
17 Failure/Error: expect(factorial(number)).to eq(expected)
18
19 expected: 1
20 got: nil
21
22 (compared using ==)
23 # ./spec/factorial_spec.rb:14:in `block (3 levels) in <top (required)>'
24
25 2) factorial returns 1 for 1
26 Failure/Error: expect(factorial(number)).to eq(expected)
27
28 expected: 1
29 got: nil
30
31 (compared using ==)
32 # ./spec/factorial_spec.rb:14:in `block (3 levels) in <top (required)>'
33
34Finished in 0.02148 seconds (files took 0.38502 seconds to load)
358 examples, 2 failures
36
37Failed examples:
38
39rspec './spec/factorial_spec.rb[1:1]' # factorial returns 1 for 0
40rspec './spec/factorial_spec.rb[1:2]' # factorial returns 1 for 1(2..0) and (2..1) are empty ranges, and reduce without a starting value returns nil for an empty range. The base-case tests caught the bug in milliseconds, long before a user could. (Since the examples come from a loop, RSpec names them by position, like [1:1], instead of by line.)
Running just some of the tests
As a suite grows, you'll often want to run one example or a few:
1$ rspec spec/factorial_spec.rb:19
2Run options: include {:locations=>{"./spec/factorial_spec.rb"=>[19]}}
3
4factorial
5 with a negative number
6 raises an ArgumentError
7
8Finished in 0.00457 seconds (files took 0.33805 seconds to load)
91 example, 0 failures1$ rspec -e "negative"
2Run options: include {:full_description=>/negative/}
3
4factorial
5 with a negative number
6 raises an ArgumentError
7
8Finished in 0.00404 seconds (files took 0.47814 seconds to load)
91 example, 0 failuresrspec path:lineruns the example (or group) at that line.rspec -e "words"runs the examples whose full description contains those words.rspec --format progressoverrides.rspecand prints just dots:........
Matchers: more ways to check
eq is only the beginning. RSpec comes with matchers for numbers, collections, strings, types, errors and changes:
1RSpec.describe "everyday matchers" do
2 it "compares values" do
3 expect(2 + 2).to eq(4)
4 expect(10).to be > 3
5 expect(7).to be_between(1, 10)
6 expect(0.1 + 0.2).to be_within(0.001).of(0.3)
7 end
8
9 it "checks true, false and nil" do
10 expect([].empty?).to be true
11 expect([]).to be_empty
12 expect(nil).to be_nil
13 expect(5).not_to be_nil
14 end
15
16 it "looks inside strings and collections" do
17 expect("Hello, Ruby").to include("Ruby")
18 expect("Hello, Ruby").to start_with("Hello")
19 expect("2024-07-07").to match(/\A\d{4}-\d{2}-\d{2}\z/)
20 expect([3, 1, 2]).to contain_exactly(1, 2, 3)
21 expect([2, 4, 6]).to all(be_even)
22 expect({ name: "Ada", born: 1815 }).to include(name: "Ada")
23 end
24
25 it "checks types" do
26 expect(42).to be_an(Integer)
27 expect("42").to be_a(String)
28 end
29
30 it "checks errors and changes" do
31 expect { Integer("forty-two") }.to raise_error(ArgumentError)
32 cart = []
33 expect { cart << "apple" }.to change { cart.size }.from(0).to(1)
34 end
35end1$ rspec
2
3everyday matchers
4 compares values
5 checks true, false and nil
6 looks inside strings and collections
7 checks types
8 checks errors and changes
9
10Finished in 0.03413 seconds (files took 0.41136 seconds to load)
115 examples, 0 failuresA few worth pointing out:
be_within(…).of(…)compares decimals with a tolerance, because0.1 + 0.2is0.30000000000000004, not exactly0.3.be_empty,be_evenare predicate matchers:be_xyzcalls the methodxyz?and expects true.not_toflips any matcher.
Good matchers also write good failure messages:
1RSpec.describe "failure messages" do
2 it "explains a missing item" do
3 expect(["apple", "banana"]).to include("cherry")
4 end
5
6 it "shows a diff for hashes" do
7 expect({ name: "Ada", born: 1815 }).to eq({ name: "Ada", born: 1816 })
8 end
9end1$ rspec spec/failing_spec.rb
2
3failure messages
4 explains a missing item (FAILED - 1)
5 shows a diff for hashes (FAILED - 2)
6
7Failures:
8
9 1) failure messages explains a missing item
10 Failure/Error: expect(["apple", "banana"]).to include("cherry")
11 expected ["apple", "banana"] to include "cherry"
12 # ./spec/failing_spec.rb:3:in `block (2 levels) in <top (required)>'
13
14 2) failure messages shows a diff for hashes
15 Failure/Error: expect({ name: "Ada", born: 1815 }).to eq({ name: "Ada", born: 1816 })
16
17 expected: {:born=>1816, :name=>"Ada"}
18 got: {:born=>1815, :name=>"Ada"}
19
20 (compared using ==)
21
22 Diff:
23 @@ -1,2 +1,2 @@
24 -:born => 1816,
25 +:born => 1815,
26 :name => "Ada",
27 # ./spec/failing_spec.rb:7:in `block (2 levels) in <top (required)>'
28
29Finished in 0.02796 seconds (files took 0.3225 seconds to load)
302 examples, 2 failures
31
32Failed examples:
33
34rspec ./spec/failing_spec.rb:2 # failure messages explains a missing item
35rspec ./spec/failing_spec.rb:6 # failure messages shows a diff for hashesThe diff points straight at the one value that differs, which is a lifesaver with big hashes.

How fast are unit tests?
Fast tests get run; slow tests get skipped. We generated specs with 10 to 10,000 factorial examples and timed them:

About 56 microseconds per example. Starting Ruby and loading files cost more (about a third of a second per run) than running a thousand examples. Small, focused unit tests like ours are cheap enough to run every time you save a file.
Your turn: practice
Try these before peeking at the answers. Set up each one like the factorial project: the code in the project folder, the spec in spec/, and --format documentation in .rspec.
Exercise 1: Predict the failure (warm-up). Which example fails, and what will RSpec say "got"? (A year is a leap year if it's divisible by 4, except years divisible by 100, unless they're also divisible by 400.)
def leap_year?(year)
year % 4 == 0
end1require_relative "../leap_year"
2
3RSpec.describe "leap_year?" do
4 it "is true for 2024" do
5 expect(leap_year?(2024)).to be true
6 end
7
8 it "is false for 2023" do
9 expect(leap_year?(2023)).to be false
10 end
11
12 it "is false for 1900" do
13 expect(leap_year?(1900)).to be false
14 end
15
16 it "is true for 2000" do
17 expect(leap_year?(2000)).to be true
18 end
19endExercise 2: Read the failure. This test fails with expected: 120, got: 0. What's the bug?
1def factorial(number)
2 product = 0
3 (1..number).each { |multiplier| product *= multiplier }
4 product
5endExercise 3: TDD FizzBuzz. Write the tests first, then the code: fizzbuzz(number) returns "Fizz" for multiples of 3, "Buzz" for multiples of 5, "FizzBuzz" for multiples of both, and the number as a string otherwise.
Exercise 4: Palindromes and edge cases. TDD a palindrome?(text) method that ignores capital letters, spaces and punctuation, so "A man, a plan, a canal: Panama!" counts. Think about the empty string too.
Exercise 5: Fibonacci (challenge). TDD fibonacci(position), where the sequence is 0, 1, 1, 2, 3, 5, 8… starting at position 0. Use a table of cases including position 100, and reject negative positions.
Quick quiz
- What are the three steps of the TDD cycle?
- Why should a new test fail before you write the code for it?
- When do you write
expect { … }with curly braces instead ofexpect(…)? - What does
be_emptyactually call? - How do you run only the examples whose names mention "negative"?
Answers
Exercise 1: 1900 is divisible by 4, so the buggy method says true:
1$ rspec
2
3leap_year?
4 is true for 2024
5 is false for 2023
6 is false for 1900 (FAILED - 1)
7 is true for 2000
8
9Failures:
10
11 1) leap_year? is false for 1900
12 Failure/Error: expect(leap_year?(1900)).to be false
13
14 expected false
15 got true
16 # ./spec/leap_year_spec.rb:13:in `block (2 levels) in <top (required)>'
17
18Finished in 0.01163 seconds (files took 0.34913 seconds to load)
194 examples, 1 failure
20
21Failed examples:
22
23rspec ./spec/leap_year_spec.rb:12 # leap_year? is false for 1900The full rule turns it green:
def leap_year?(year)
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
end1$ rspec
2
3leap_year?
4 is true for 2024
5 is false for 2023
6 is false for 1900
7 is true for 2000
8
9Finished in 0.00365 seconds (files took 0.3437 seconds to load)
104 examples, 0 failuresNotice 2000 passed even with the bug: without the 1900 test, the bug would have gone unnoticed. Good tests hunt for the tricky cases.
Exercise 2: product starts at 0, and 0 times anything is 0. A product must start at 1 (just as a sum starts at 0). Changing product = 0 to product = 1 makes it pass.
1$ rspec
2
3factorial
4 returns 120 for 5 (FAILED - 1)
5
6Failures:
7
8 1) factorial returns 120 for 5
9 Failure/Error: expect(factorial(5)).to eq(120)
10
11 expected: 120
12 got: 0
13
14 (compared using ==)
15 # ./spec/factorial_spec.rb:5:in `block (2 levels) in <top (required)>'
16
17Finished in 0.01086 seconds (files took 0.39225 seconds to load)
181 example, 1 failure
19
20Failed examples:
21
22rspec ./spec/factorial_spec.rb:4 # factorial returns 120 for 5Exercise 3: One test per rule. Check "both" first in the code, or 15 would stop at "Fizz":
1require_relative "../fizzbuzz"
2
3RSpec.describe "fizzbuzz" do
4 it "returns the number as text" do
5 expect(fizzbuzz(1)).to eq("1")
6 end
7
8 it "returns Fizz for multiples of 3" do
9 expect(fizzbuzz(9)).to eq("Fizz")
10 end
11
12 it "returns Buzz for multiples of 5" do
13 expect(fizzbuzz(10)).to eq("Buzz")
14 end
15
16 it "returns FizzBuzz for multiples of both" do
17 expect(fizzbuzz(30)).to eq("FizzBuzz")
18 end
19end1def fizzbuzz(number)
2 return "FizzBuzz" if number % 15 == 0
3 return "Fizz" if number % 3 == 0
4 return "Buzz" if number % 5 == 0
5
6 number.to_s
7end1$ rspec
2
3fizzbuzz
4 returns the number as text
5 returns Fizz for multiples of 3
6 returns Buzz for multiples of 5
7 returns FizzBuzz for multiples of both
8
9Finished in 0.00378 seconds (files took 0.33651 seconds to load)
104 examples, 0 failuresExercise 4: Each requirement became an example, plus the empty-string edge case:
1require_relative "../palindrome"
2
3RSpec.describe "palindrome?" do
4 it "is true for a simple palindrome" do
5 expect(palindrome?("level")).to be true
6 end
7
8 it "is false for a word that is not one" do
9 expect(palindrome?("ruby")).to be false
10 end
11
12 it "ignores capital letters" do
13 expect(palindrome?("Racecar")).to be true
14 end
15
16 it "ignores spaces and punctuation" do
17 expect(palindrome?("A man, a plan, a canal: Panama!")).to be true
18 end
19
20 it "is true for an empty string" do
21 expect(palindrome?("")).to be true
22 end
23end1def palindrome?(text)
2 letters = text.downcase.gsub(/[^a-z0-9]/, "")
3 letters == letters.reverse
4end1$ rspec
2
3palindrome?
4 is true for a simple palindrome
5 is false for a word that is not one
6 ignores capital letters
7 ignores spaces and punctuation
8 is true for an empty string
9
10Finished in 0.0033 seconds (files took 0.33578 seconds to load)
115 examples, 0 failures(The string methods guide covers downcase, gsub and reverse.)
Exercise 5: A table of cases plus the error case. The 21-digit answer for position 100 is no problem for Ruby:
1require_relative "../fibonacci"
2
3RSpec.describe "fibonacci" do
4 { 0 => 0, 1 => 1, 2 => 1, 3 => 2, 10 => 55, 100 => 354_224_848_179_261_915_075 }.each do |position, expected|
5 it "returns #{expected} at position #{position}" do
6 expect(fibonacci(position)).to eq(expected)
7 end
8 end
9
10 it "rejects negative positions" do
11 expect { fibonacci(-1) }.to raise_error(ArgumentError, /negative/)
12 end
13end1def fibonacci(position)
2 raise ArgumentError, "position must not be negative" if position.negative?
3
4 previous, current = 0, 1
5 position.times { previous, current = current, previous + current }
6 previous
7end1$ rspec
2
3fibonacci
4 returns 0 at position 0
5 returns 1 at position 1
6 returns 1 at position 2
7 returns 2 at position 3
8 returns 55 at position 10
9 returns 354224848179261915075 at position 100
10 rejects negative positions
11
12Finished in 0.00654 seconds (files took 0.33746 seconds to load)
137 examples, 0 failuresQuiz: (1) Red (write a failing test), green (make it pass simply), refactor (clean up while staying green). (2) A test that has never failed might not be checking anything; seeing it fail proves it can catch the problem. (3) When you're checking something that happens while the code runs, like raise_error or change: RSpec needs the code as a block so it can run it itself. (4) The empty? method: be_xyz calls xyz?. (5) rspec -e "negative".
Cheat sheet
| I want to… | Write |
|---|---|
| Install RSpec | gem install rspec |
| Set up a project | rspec --init (creates .rspec and spec/spec_helper.rb) |
| Group the tests for something | RSpec.describe "factorial" do … end |
| Group tests for one situation | context "with a negative number" do … end |
| Write one test | it "returns 1 for 0" do … end |
| Check a value | expect(factorial(0)).to eq(1) |
| Check the opposite | expect(value).not_to be_nil |
| Check for an error | expect { code }.to raise_error(ArgumentError) |
| Run all tests | rspec (from the folder that holds spec/) |
| Run one file / one line | rspec spec/factorial_spec.rb / rspec spec/factorial_spec.rb:19 |
| Run tests by name | rspec -e "negative" |
| Show test names | --format documentation in .rspec |
Wrapping up
Testing turns "I think it works" into "I know it works, and I'll know the moment it doesn't":
- TDD is a loop: red (a failing test), green (the simplest passing code), refactor (clean up while staying green).
- Let failures guide you. Each message (no method, wrong arguments, wrong value) tells you the next small step.
- Fake it, then triangulate. Add examples until only the real solution can pass them all.
- Specs read like sentences:
describewhat,itdoes something,expect(actual).to matcher. - Tests are a safety net: they let you refactor boldly and catch bugs like the empty-range
nilin milliseconds. - Think about edge cases: zero, one, huge values, negative or empty input.
- Unit tests are fast: about 56 microseconds per example in our measurement.
Want more Ruby? Read What Is the Ruby Programming Language?, then Ruby Arrays, Ruby Hashes and Ruby String Methods.
Looking for a reference text for Ruby? Get the Well-Grounded Rubyist.
Read next

Ruby Blocks and Iterators: Write Your Own
Learn how Ruby methods receive blocks, pass values with yield, handle a missing block, and return an Enumerator. Build and run your own iterator.

C++ std::vector: Size, Capacity, and Safe Growth
Learn how std::vector grows in C++: the difference between size and capacity, when reserve helps, why resize is different, and how growth can invalidate references.

How to Handle Errors in Go: A Hands-On Guide
Learn Go's everyday error pattern with runnable examples: return an error, check it, add context with %w, and recognize a missing file with errors.Is.
Comments
Be kind and specific. Comments with links from new accounts appear after a moderator reviews them.