Testing with RSpec
## Learning Objectives
- Understand testing fundamentals
- Write RSpec tests
- Use matchers effectively
- Organize tests with describe and context
## Testing Fundamentals
### Why Test?
- Catch bugs early
- Prevent regressions
- Document behavior
- Enable refactoring with confidence
### Types of Tests
| Type | Description |
|------|-------------|
| Unit | Test individual methods |
| Integration | Test interactions between components |
| Functional | Test features end-to-end |
| Performance | Test speed and scalability |
## RSpec Overview
RSpec is Ruby's most popular BDD testing framework.
### Installation
```ruby
# Gemfile
group :test do
gem 'rspec'
end
```
```bash
bundle install
rspec --init
```
## Basic Structure
### Spec File
```ruby
# spec/calculator_spec.rb
RSpec.describe Calculator do
describe '#add' do
it 'returns the sum of two numbers' do
calc = Calculator.new
expect(calc.add(2, 3)).to eq(5)
end
end
end
```
### Run Tests
```bash
rspec
rspec spec/calculator_spec.rb
rspec spec/calculator_spec.rb:10 # Run specific test
```
## Describe and Context
```ruby
RSpec.describe Order do
describe '#total' do
context 'when order is empty' do
it 'returns zero' do
order = Order.new
expect(order.total).to eq(0)
end
end
context 'when order has items' do
it 'returns sum of item prices' do
order = Order.new
order.add_item(Item.new(price: 10))
order.add_item(Item.new(price: 20))
expect(order.total).to eq(30)
end
end
end
end
```
## Matchers
### Equality
```ruby
expect(actual).to eq(expected) # Equal value
expect(actual).to eql(expected) # Equal value (less strict)
expect(actual).to be(expected) # Same object
expect(actual).to equal(expected) # Same object (identity)
```
### Truthiness
```ruby
expect(actual).to be_truthy
expect(actual).to be_falsey
expect(actual).to be_nil
expect(actual).to be true
expect(actual).to be false
```
### Comparisons
```ruby
expect(actual).to be > value
expect(actual).to be >= value
expect(actual).to be < value
expect(actual).to be <= value
expect(actual).to be_between(min, max)
expect(actual).to match(/pattern/)
```
### Collections
```ruby
expect(actual).to include(element)
expect(actual).to contain_exactly(element1, element2)
expect(actual).to start_with(item)
expect(actual).to end_with(item)
expect(actual).to have_attributes(key: value)
```
### Types
```ruby
expect(actual).to be_a(Class)
expect(actual).to be_an_instance_of(Class)
expect(actual).to respond_to(:method_name)
```
### Errors
```ruby
expect { code }.to raise_error
expect { code }.to raise_error(ErrorClass)
expect { code }.to raise_error('message')
expect { code }.to raise_error(ErrorClass, 'message')
```
### Predicates
```ruby
expect(actual).to be_empty
expect(actual).to be_valid
expect(array).to be_an(Array)
```
## Before and After Hooks
```ruby
RSpec.describe User do
before(:each) do
@user = User.create(name: 'Alice')
end
after(:each) do
User.destroy_all
end
before(:all) do
@admin = User.create(name: 'Admin', role: 'admin')
end
after(:all) do
User.destroy_all
end
# Or with :context for per-context setup
before(:context) do
# Runs once before all examples in this group
end
end
```
## Let and Let
```ruby
RSpec.describe User do
# lazy evaluation (only created when used)
let(:user) { User.create(name: 'Alice') }
# eager evaluation (created before each example)
let!(:post) { Post.create(title: 'Test', author: 'Alice') }
it 'has posts' do
expect(user.posts).to include(post)
end
end
```
## Shared Examples
```ruby
RSpec.shared_examples 'a countable thing' do
it 'starts at zero' do
expect(subject.count).to eq(0)
end
it 'increments count' do
subject.increment
expect(subject.count).to eq(1)
end
end
RSpec.describe Counter do
it_behaves_like 'a countable thing'
end
RSpec.describe List do
it_behaves_like 'a countable thing'
end
```
## Focus and Skip
```ruby
# Focus - run only this test
it 'this test', focus: true do
expect(true).to be true
end
# Skip - don't run this test
it 'will not run', skip: true do
expect(true).to be false
end
# Pending - runs but doesn't fail
it 'is pending' do
skip 'reason'
end
```
## Test Doubles
### Dummy
```ruby
let(:dummy) { Object.new }
```
### Fake
```ruby
fake_user_repo = FakeUserRepository.new
```
### Stub
```ruby
allow(Object).to receive(:method).and_return(value)
allow(Object).to receive_messages(method1: value1, method2: value2)
```
### Mock
```ruby
expect(Object).to receive(:method).with(args).and_return(value)
expect(Object).to receive(:method).exactly(3).times
expect(Object).to receive(:method).at_least(:once)
```
### Spy
```ruby
Object spy: Object.new
allow(Object).to receive(:method)
Object.method
expect(Object).to have_received(:method)
```
## Feature Specs (Capybara)
```ruby
require 'capybara/rspec'
RSpec.describe 'User registration' do
scenario 'user signs up successfully' do
visit '/users/new'
fill_in 'Name', with: 'Alice'
fill_in 'Email', with: 'alice@example.com'
click_button 'Sign Up'
expect(page).to have_content('Welcome, Alice!')
end
end
```
## Test Organization
```text
spec/
├── spec_helper.rb # Shared configuration
├── support/ # Helper modules
│ └── factories.rb
├── models/
│ ├── user_spec.rb
│ └── post_spec.rb
└── features/
└── user_signup_spec.rb
```
### spec_helper.rb
```ruby
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
config.shared_context_metadata_behavior = :apply_to_host_groups
config.filter_run_when_matching :focus
config.example_status_persistence_file_path = 'spec/examples.txt'
config.disable_monkey_patching!
config.order = :random
Kernel.srand config.seed
end
```
## Summary
- RSpec uses `describe` and `context` to group tests
- `it` defines a test case
- Matchers: `eq`, `be`, `include`, `match`, `raise_error`, etc.
- `before`/`after` hooks for setup/teardown
- `let` and `let!` for test data
- Test doubles: dummy, fake, stub, mock, spy
- Shared examples for reusable test logic
- `rspec --init` to initialize RSpec
Comments
Comments powered by Giscus
To enable comments, add your Giscus embed code here.
Learn more about Giscus →