Skip to content

Commit bab5e6b

Browse files
committed
Up
1 parent 7d155ce commit bab5e6b

File tree

2 files changed

+296
-22
lines changed

2 files changed

+296
-22
lines changed
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
---
2+
title: "Testing Ruby Code with RSpec: A Comprehensive Guide"
3+
date: 2025-09-13 11:20:00 -0800
4+
categories: [Programming, Ruby, Testing]
5+
tags: [ruby, rspec, testing, tdd, bdd]
6+
author: dain
7+
---
8+
9+
RSpec is Ruby's most popular testing framework, providing a domain-specific language for behavior-driven development. Let's explore how to write effective tests with RSpec.
10+
11+
## Getting Started with RSpec
12+
13+
Add RSpec to your Gemfile:
14+
15+
```ruby
16+
group :development, :test do
17+
gem 'rspec-rails' # For Rails apps
18+
# or
19+
gem 'rspec' # For non-Rails apps
20+
end
21+
```
22+
23+
Initialize RSpec:
24+
25+
```bash
26+
bundle install
27+
rspec --init
28+
```
29+
30+
This creates a `spec` directory and `spec_helper.rb` file.
31+
32+
## Basic RSpec Structure
33+
34+
RSpec tests are organized with `describe` and `it` blocks:
35+
36+
```ruby
37+
# spec/calculator_spec.rb
38+
require 'spec_helper'
39+
require_relative '../lib/calculator'
40+
41+
RSpec.describe Calculator do
42+
describe '#add' do
43+
it 'returns the sum of two numbers' do
44+
calculator = Calculator.new
45+
result = calculator.add(2, 3)
46+
expect(result).to eq(5)
47+
end
48+
49+
it 'handles negative numbers' do
50+
calculator = Calculator.new
51+
result = calculator.add(-2, 3)
52+
expect(result).to eq(1)
53+
end
54+
end
55+
56+
describe '#divide' do
57+
it 'returns the quotient of two numbers' do
58+
calculator = Calculator.new
59+
result = calculator.divide(10, 2)
60+
expect(result).to eq(5)
61+
end
62+
63+
it 'raises an error when dividing by zero' do
64+
calculator = Calculator.new
65+
expect { calculator.divide(10, 0) }.to raise_error(ZeroDivisionError)
66+
end
67+
end
68+
end
69+
```
70+
71+
## RSpec Matchers
72+
73+
RSpec provides many built-in matchers:
74+
75+
```ruby
76+
# Equality
77+
expect(actual).to eq(expected)
78+
expect(actual).to eql(expected)
79+
expect(actual).to be(expected)
80+
81+
# Comparison
82+
expect(actual).to be > 3
83+
expect(actual).to be_between(1, 10)
84+
85+
# Type checking
86+
expect(actual).to be_a(String)
87+
expect(actual).to be_an(Integer)
88+
expect(actual).to be_instance_of(String)
89+
90+
# Truthiness
91+
expect(actual).to be_truthy
92+
expect(actual).to be_falsy
93+
expect(actual).to be_nil
94+
95+
# Collections
96+
expect(actual).to include('item')
97+
expect(actual).to contain_exactly(1, 2, 3)
98+
expect(actual).to match_array([3, 1, 2])
99+
100+
# Strings
101+
expect(actual).to start_with('Hello')
102+
expect(actual).to end_with('world')
103+
expect(actual).to match(/regex/)
104+
105+
# Exceptions
106+
expect { some_code }.to raise_error(StandardError)
107+
expect { some_code }.to raise_error(StandardError, 'message')
108+
expect { some_code }.not_to raise_error
109+
```
110+
111+
## Before and After Hooks
112+
113+
Set up and tear down test data:
114+
115+
```ruby
116+
RSpec.describe User do
117+
before(:each) do
118+
@user = User.new(name: 'John', email: '[email protected]')
119+
end
120+
121+
after(:each) do
122+
# Cleanup code
123+
end
124+
125+
before(:all) do
126+
# Run once before all tests in this describe block
127+
end
128+
129+
after(:all) do
130+
# Run once after all tests in this describe block
131+
end
132+
133+
it 'has a valid name' do
134+
expect(@user.name).to eq('John')
135+
end
136+
end
137+
```
138+
139+
## Subject and Let
140+
141+
Use `let` for lazy-loaded variables and `subject` for the main object under test:
142+
143+
```ruby
144+
RSpec.describe User do
145+
let(:user) { User.new(name: 'John', email: '[email protected]') }
146+
let!(:admin) { User.create(name: 'Admin', admin: true) } # let! is eager-loaded
147+
148+
subject { user }
149+
150+
it 'has a name' do
151+
expect(subject.name).to eq('John')
152+
end
153+
154+
it { is_expected.to be_valid }
155+
it { is_expected.not_to be_admin }
156+
end
157+
```
158+
159+
## Shared Examples
160+
161+
Reuse test logic across multiple specs:
162+
163+
```ruby
164+
# spec/support/shared_examples.rb
165+
RSpec.shared_examples 'a valid email' do
166+
it 'accepts valid email formats' do
167+
valid_emails = ['[email protected]', '[email protected]']
168+
valid_emails.each do |email|
169+
subject.email = email
170+
expect(subject).to be_valid
171+
end
172+
end
173+
174+
it 'rejects invalid email formats' do
175+
invalid_emails = ['invalid', '@example.com', 'test@']
176+
invalid_emails.each do |email|
177+
subject.email = email
178+
expect(subject).not_to be_valid
179+
end
180+
end
181+
end
182+
183+
# Usage in specs
184+
RSpec.describe User do
185+
subject { User.new(name: 'John') }
186+
187+
it_behaves_like 'a valid email'
188+
end
189+
```
190+
191+
## Mocking and Stubbing
192+
193+
RSpec provides built-in mocking capabilities:
194+
195+
```ruby
196+
RSpec.describe EmailService do
197+
let(:mailer) { double('Mailer') }
198+
let(:service) { EmailService.new(mailer) }
199+
200+
describe '#send_welcome_email' do
201+
it 'sends an email to the user' do
202+
user = User.new(email: '[email protected]')
203+
204+
expect(mailer).to receive(:send_email)
205+
.with(user.email, 'Welcome!')
206+
.and_return(true)
207+
208+
result = service.send_welcome_email(user)
209+
expect(result).to be true
210+
end
211+
end
212+
213+
describe '#user_count' do
214+
it 'returns the number of users' do
215+
allow(User).to receive(:count).and_return(42)
216+
217+
expect(service.user_count).to eq(42)
218+
end
219+
end
220+
end
221+
```
222+
223+
## Testing Class Methods
224+
225+
```ruby
226+
RSpec.describe User do
227+
describe '.find_by_email' do
228+
it 'returns a user with the given email' do
229+
user = User.create(email: '[email protected]')
230+
231+
found_user = User.find_by_email('[email protected]')
232+
expect(found_user).to eq(user)
233+
end
234+
235+
it 'returns nil if no user found' do
236+
result = User.find_by_email('[email protected]')
237+
expect(result).to be_nil
238+
end
239+
end
240+
end
241+
```
242+
243+
## Custom Matchers
244+
245+
Create your own matchers for domain-specific assertions:
246+
247+
```ruby
248+
# spec/support/matchers.rb
249+
RSpec::Matchers.define :be_a_valid_email do
250+
match do |actual|
251+
actual =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
252+
end
253+
254+
failure_message do |actual|
255+
"expected '#{actual}' to be a valid email address"
256+
end
257+
end
258+
259+
# Usage
260+
expect('[email protected]').to be_a_valid_email
261+
expect('invalid-email').not_to be_a_valid_email
262+
```
263+
264+
## Running Tests
265+
266+
```bash
267+
# Run all tests
268+
rspec
269+
270+
# Run specific file
271+
rspec spec/models/user_spec.rb
272+
273+
# Run specific test
274+
rspec spec/models/user_spec.rb:25
275+
276+
# Run with specific format
277+
rspec --format documentation
278+
rspec --format json
279+
280+
# Run with tags
281+
rspec --tag focus
282+
rspec --tag ~slow # Exclude slow tests
283+
```
284+
285+
## Best Practices
286+
287+
1. **Use descriptive test names** that explain the behavior
288+
2. **Follow AAA pattern** (Arrange, Act, Assert)
289+
3. **One assertion per test** when possible
290+
4. **Use `let` for setup** instead of instance variables
291+
5. **Test behavior, not implementation**
292+
6. **Use shared examples** for common behavior
293+
7. **Mock external dependencies**
294+
8. **Keep tests fast** by avoiding unnecessary database hits
295+
296+
RSpec makes testing Ruby code enjoyable and expressive. Good tests give you confidence to refactor and add new features!

_posts/Rspec testing.md

Lines changed: 0 additions & 22 deletions
This file was deleted.

0 commit comments

Comments
 (0)