Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
103: Shared Examples in RSpec
You've probably noticed by now that in a growing codebase, you often end up with different classes that are supposed to "act" the same way. In Ruby, we love duck typing—if it walks like a duck and quacks like a duck, it's a duck. But when it comes to testing, that often leads to a boring kind of repetition: writing the exact same five tests for three different classes just because they all implement the same interface.
That's where shared examples come in. Instead of copy-pasting tests, we define the "behavior" once and tell RSpec to apply it to any class that should adhere to that contract.
The problem: Repeating ourselves with User and Project
Let's say we're building a system where both User and Project accounts can be "activated" or "deactivated". I've written a Togglable module to handle this, and I want to make sure both classes handle the state transition correctly.
module Togglable
def toggle!
@active = !@active
end
def active?
!!@active
end
end
class User
include Togglable
def initialize; @active = false; end
end
class Project
include Togglable
def initialize; @active = false; end
end
If I write my tests the traditional way, my spec file starts looking like this:
RSpec.describe User do
it "starts inactive" do
expect(User.new).not_to be_active
end
it "toggles activity" do
user = User.new
user.toggle!
expect(user).to be_active
end
end
RSpec.describe Project do
it "starts inactive" do
expect(Project.new).not_to be_active
end
it "toggles activity" do
project = Project.new
project.toggle!
expect(project).to be_active
end
end
This is fine for two classes, but if I add Organization, Task, and Folder, I'm just wasting time. It's also a maintenance nightmare; if the logic for toggle! changes, I have to update tests in five different files.
Defining the shared behavior
I'm going to move these common tests into a shared_examples block. I'll give it a descriptive name so that when the tests fail, the output tells me exactly which "behavior" is broken.
RSpec.shared_examples "a togglable object" do
it "starts inactive" do
expect(subject).not_to be_active
end
it "toggles activity" do
subject.toggle!
expect(subject).to be_active
end
end
Now, I can use it_behaves_like inside my class specs. This tells RSpec: "Take all the tests inside 'a togglable object' and run them right here, using the current subject."
RSpec.describe User do
subject { User.new }
it_behaves_like "a togglable object"
end
RSpec.describe Project do
subject { Project.new }
it_behaves_like "a togglable object"
end
Wait, why is the Project test failing?
Here is where I usually trip up when I'm moving too fast. I decided to add a test to the shared example to ensure that toggling an object also updates a updated_at timestamp. I wrote it like this:
RSpec.shared_examples "a togglable object" do
# ... previous tests ...
it "updates the timestamp" do
expect { subject.toggle! }.to change { subject.updated_at }.from(nil)
end
end
Immediately, the User tests pass, but the Project tests crash with a NoMethodError: undefined method 'updated_at'. I forgot that while both classes are Togglable, only the User class had the updated_at attribute implemented. I tried to force a specific implementation detail into a general behavior spec.
The fix is to remember that shared examples should only test the interface (the "contract"), not the internal implementation of every class that uses it. If updated_at is only for Users, it belongs in user_spec.rb, not the shared example. I'll move that specific test back to the User block and keep the shared examples lean.
Tying it all together with it_behaves_like
By separating the "what it does" (the shared example) from the "who does it" (the class spec), the code becomes much cleaner. If I create a new Organization class tomorrow, I just add one line to its spec, and I instantly know it adheres to the Togglable contract. It's a powerful way to enforce consistency across your Ruby objects without bloating your test suite.
📋 Practical Task
Exercise: Implementing a Searchable Interface for Product and Category
You are building an e-commerce platform. Both the Product and Category classes must implement a searchable_name method that returns a lowercase string used for indexing in a search engine.
Your task:
- Create a shared example called
"a searchable object". - The shared example should contain two tests:
- It verifies that
searchable_namereturns a string. - It verifies that
searchable_nameis always lowercase.
- It verifies that
- Implement the
ProductandCategoryclasses so that they both respond tosearchable_name. - Use
it_behaves_likein the specs for both classes to ensure they both adhere to the searchable contract.
There are no comments for now.