← Ruby EnglishChapter 12 of 13

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 →