class Animal attr_accessor :name # get + set attr_reader :species # get only attr_writer :age # set only def initialize(name, species) @name = name @species = species end def to_s "#{@name} (#{@species})" end end a = Animal.new('Rex', 'Dog') a.name # 'Rex' a.name = 'Max' # setter
class Dog < Animal def initialize(name) super(name, 'Canine') @tricks = [] end def to_s super + " [dog]" end end dog = Dog.new('Rex') dog.is_a?(Dog) # true dog.is_a?(Animal) # true dog.instance_of?(Dog) # true (exact class) dog.instance_of?(Animal) # false dog.class # Dog Dog.superclass # Animal
class Counter @@count = 0 # class variable (shared!) def initialize @@count += 1 end def self.count @@count end end Counter.new; Counter.new Counter.count # 2 # class << self (multiple class methods) class Foo class << self def bar; 'bar'; end def baz; 'baz'; end end end
class Person def greet; "Hi, #{full_name}"; end # public protected def compare(other) # same class/subclass only name == other.name end private def full_name # no explicit receiver "#{first} #{last}" end end # Bypass private in tests: obj.send(:private_method)
class Box include Comparable attr_accessor :volume def initialize(v); @volume = v; end def <=>(other) volume <=> other.volume end # Gives: < > <= >= between? clamp sort min max end boxes = [Box.new(3), Box.new(1), Box.new(2)] boxes.sort.map(&:volume) # [1, 2, 3] boxes.min.volume # 1
class Ghost def method_missing(name, *args) if name.to_s.start_with?('say_') word = name.to_s.sub('say_', '') "#{word}!" else super end end def respond_to_missing?(name, include_private = false) name.to_s.start_with?('say_') || super end end Ghost.new.say_hello # "hello!" Ghost.new.respond_to?(:say_hi) # true
a = [1, 2, 3] a = Array.new(3, 0) # [0, 0, 0] a = Array.new(3) { |i| i*2 } # [0, 2, 4] a[0] / a[-1] # first / last a[1, 2] # [2,3] start+length a[1..2] # [2,3] range a.first(2) / a.last(2) a.push(4) / a << 4 # append a.pop # remove+return last a.shift / a.unshift(0) # remove/prepend first a.flatten # nested → flat a.compact # remove nils a.uniq # remove duplicates a.flatten(1) # one level only
arr.map { |x| x * 2 } # transform arr.select { |x| x > 2 } # filter arr.reject { |x| x > 2 } # inverse filter arr.find { |x| x > 2 } # first match arr.all? { |x| x > 0 } # all satisfy? arr.any? { |x| x > 5 } # any satisfy? arr.none? { |x| x < 0 } # none satisfy? arr.count { |x| x.even? } # count matches arr.reduce(0) { |acc, x| acc + x } arr.flat_map { |x| [x, x*2] } arr.each_with_object([]) { |x, a| a << x.to_s } arr.each_with_index { |x, i| ... } arr.zip([4,5,6]) # [[1,4],[2,5],[3,6]] arr.group_by { |x| x % 2 } # {0=>[2], 1=>[1,3]} arr.tally # {"a"=>2, "b"=>1}
h = { name: 'Eric', age: 30 }
h[:name]
h.fetch(:name) # raises if missing
h.fetch(:x, 'default')
h.dig(:user, :address, :city)
h.merge({ city: 'DC' })
h.merge!({ city: 'DC' })
h.select { |k,v| v > 18 }
h.reject { |k,v| v.nil? }
h.transform_values { |v| v.to_s }
h.transform_keys { |k| k.to_s }
h.slice(:name, :age)
h.each { |k, v| ... }
h.map { |k, v| [k, v*2] }.to_h
h.any? { |k, v| v > 20 }
(1..5).to_a # [1,2,3,4,5] inclusive (1...5).to_a # [1,2,3,4] exclusive (1..10).cover?(5.5) # true (no iteration) (1..10).step(2).to_a # [1,3,5,7,9] arr.sort arr.sort_by { |x| x.name } arr.sort_by { |x| [x.a, x.b] } # multi-key arr.sort_by { |x| -x.score } # descending arr.sort_by { |x| [-x.priority, x.created_at] } # priority DESC, created_at ASC arr.sort { |a, b| [b.priority, a.created_at] <=> [a.priority, b.created_at] } # tuple compare arr.min_by / arr.max_by { |x| x.age } # Spaceship 1 <=> 2 # -1 2 <=> 2 # 0 3 <=> 2 # 1
# Parallel assignment a, b, c = [1, 2, 3] first, *rest = [1, 2, 3, 4] # rest = [2,3,4] *init, last = [1, 2, 3, 4] # init = [1,2,3] a, (b, c), d = [1, [2, 3], 4] # nested destructure # Splat in methods def sum(*nums); nums.sum; end sum(1, 2, 3) # 6 # Double splat (keyword args) def greet(name:, greeting: 'Hello') "#{greeting}, #{name}" end opts = { name: 'Eric', greeting: 'Hey' } greet(**opts) # "Hey, Eric" # Spread array as args args = [1, 2, 3] some_method(*args)
# Inline block [1,2,3].each { |x| puts x } # do...end (multi-line) [1,2,3].each do |x| puts x * 2 end # yield def twice yield yield end twice { puts 'hi' } # yield with args + guard def transform(x) yield(x) if block_given? end transform(5) { |n| n * 2 } # 10
# Proc — loose args, returns from enclosing method p = proc { |x| x * 2 } p.call(5) / p.(5) / p[5] # all valid # Lambda — strict args, returns from lambda only l = lambda { |x| x * 2 } l = ->(x) { x * 2 } # stabby lambda l.lambda? # true # Currying add = ->(a, b) { a + b } add5 = add.curry.call(5) add5.call(3) # 8
# Symbol to proc ['a', 'b'].map(&:upcase) # ['A', 'B'] [1, 2, 3].select(&:odd?) # [1, 3] ['1','2'].map(&:to_i) # [1, 2] # Convert lambda to block double = ->(x) { x * 2 } [1,2,3].map(&double) # [2, 4, 6] # Capture block as proc def capture(&block) block.call(42) end capture { |n| n * 2 } # 84 # Method objects as blocks m = method(:puts) [1,2,3].each(&m)
| Proc | Lambda | |
|---|---|---|
| Arity check | No — extra args silently nil | Yes — raises ArgumentError |
return | Returns from enclosing method | Returns from lambda only |
lambda? | false | true |
| Creation | proc { } / Proc.new { } | lambda { } / ->{ } |
| Best for | Callbacks, block capture | Function values, currying |
x = 10 add_x = ->(n) { n + x } add_x.call(5) # 15 x = 20 add_x.call(5) # 25 ← sees updated x! # Closures capture the binding, not the value fns = (1..3).map { |i| -> { i * 2 } } fns[0].call # 2 fns[1].call # 4 fns[2].call # 6
s = "Hello, World" s.upcase / s.downcase / s.swapcase s.capitalize # "Hello, world" s.length / s.size s.reverse s.strip / s.lstrip / s.rstrip s.chomp # remove trailing \n s.chop # remove last char s.include?('World') s.start_with?('Hello') s.end_with?('World') s.split(', ') # ["Hello", "World"] s.gsub('l', 'r') # "Herro, Worrd" s.gsub(/\d+/, 'NUM') s.scan(/\w+/) # array of matches s.match?(/Hello/) # true, no MatchData
name = "Eric" "Hello #{name.upcase}" 'Hello #{name}' # no interpolation %Q{Hello #{name}} # like double-quote %q{No #{interp}} # like single-quote text = <<~HEREDOC Line one Line #{name} HEREDOC # <<~ strips leading whitespace # Frozen string literal (top of file) # frozen_string_literal: true
:name.to_s # "name" "name".to_sym # :name %i[foo bar baz] # [:foo, :bar, :baz] # Symbols: same obj always :foo.object_id == :foo.object_id # true "foo".object_id == "foo".object_id # false # Regex str =~ /\d+/ # index or nil m = str.match(/(\d{4})/) m[1] # capture group m = str.match(/(?<yr>\d{4})/) m[:yr] # named capture str.gsub(/\d+/) { |n| n.to_i * 2 }
| String | Symbol | Frozen String | |
|---|---|---|---|
| Mutable | Yes (default) | No | No |
| Identity | New obj each time | Same obj always | May be deduped |
| GC pressure | Higher | Lower (interned) | Lower |
| Use for | User data, output | Keys, identifiers | Constants, keys |
| Freeze | "str".freeze | Always frozen | # frozen_string_literal: true |
module M def hello; "hello from M"; end end # include → instance methods class A; include M; end A.new.hello # extend → class methods class B; extend M; end B.hello # extend on single object obj = Object.new; obj.extend(M) obj.hello # prepend → before class in lookup chain class C; prepend M; end # M#hello called first; use super to reach C # Used for: decorators, logging, memoization
module Trackable extend ActiveSupport::Concern included do before_create :set_uuid scope :recent, -> { order(created_at: :desc) } end class_methods do def find_by_uuid(uuid) find_by!(uuid: uuid) end end def set_uuid self.uuid ||= SecureRandom.uuid end end class Post < ApplicationRecord include Trackable end
class Child < Parent include ModA prepend ModB end Child.ancestors # [ModB, Child, ModA, Parent, # Object, Kernel, BasicObject] Child.instance_methods(false) # own only Child.method_defined?(:foo) # incl. inherited # super travels up chain def foo; super; end # passes same args def foo; super(); end # explicit no args
class WordCollection include Enumerable def initialize; @words = []; end def add(w); @words << w; end def each(&block) @words.each(&block) end end wc = WordCollection.new wc.add('hello'); wc.add('world') wc.map(&:upcase) # ['HELLO', 'WORLD'] wc.sort # sorted array wc.min / wc.max
# Struct: lightweight value object Point = Struct.new(:x, :y) p = Point.new(1, 2) p.x / p.to_h / p.members # 1 / {x:1,y:2} / [:x,:y] # With extra methods Point = Struct.new(:x, :y) do def distance; Math.sqrt(x**2 + y**2); end end # Struct equality: value-based Point.new(1,2) == Point.new(1,2) # true! # OpenStruct: dynamic (avoid in perf-critical code) require 'ostruct' o = OpenStruct.new(name: 'Eric') o.anything = 'dynamic!'
puts 'hi' unless admin? sleep 1 until ready? return nil if value.blank? x > 0 ? 'pos' : 'neg' case obj when String then 'string' when Integer then 'int' when /foo/ then 'matches foo' when (1..10) then 'in range' when ->(x) { x > 100 } then 'big' else 'other' end # case uses === for each when
begin risky_call rescue ArgumentError => e puts e.message rescue RuntimeError, TypeError => e log(e) retry # re-run begin block else puts 'no error' # runs only if no exception ensure cleanup # always runs end raise ArgumentError, 'bad input' raise # re-raise current class PaymentError < StandardError; end # ⚠ Rescue StandardError, not Exception
# ONLY nil and false are falsy! # 0, "", [], {} are ALL truthy == # value equality (overridable) .equal? # object identity (same object_id) .eql? # strict equality (used in Hash) === # case subsumption (when uses this) 1 == 1.0 # true (coerces) 1.eql?(1.0) # false (strict) :foo.equal?(:foo) # true (interned) "a".equal?("a") # false (diff objects) nil.nil? # true false.nil? # false 0.nil? # false ← gotcha!
user&.profile&.avatar_url # nil if any step nil x.nil? # only true for nil x.blank? # Rails: nil,""," ",[],{} x.present? # Rails: !blank? x.presence # returns x or nil if blank # Default assignment @val ||= expensive() # memoize hash[:key] ||= [] # ensure default # Null object pattern user = User.find_by(id: id) || NullUser.new
| Gotcha | Explanation | Fix |
|---|---|---|
0 is truthy | Only nil and false are falsy | Use .nil? or .zero? explicitly |
array = array + [x] | Creates new array, original unchanged | Use array << x or array.push(x) |
hash.merge | Non-destructive — returns new hash | Use merge! to mutate in place |
def method(x=[]) | Default args evaluated once in Ruby | Use x = nil; x ||= [] inside body |
@@class_var | Shared across ALL subclasses | Use @class_instance_var on class instead |
p.return in proc | Returns from enclosing method, not proc | Use lambda instead of proc |
"str" == :sym | Always false — different types | Use .to_sym / .to_s to convert |
# spec/models/user_spec.rb RSpec.describe User do describe '#full_name' do context 'when first and last name set' do it 'returns full name' do user = User.new(first: 'Eric', last: 'B') expect(user.full_name).to eq('Eric B') end end context 'when last name missing' do it 'returns first name only' do user = User.new(first: 'Eric') expect(user.full_name).to eq('Eric') end end end end
before(:each) { @user = User.create!(name: 'Eric') } before(:all) { # runs once before describe block } after(:each) { # cleanup per example } around(:each) { |ex| setup; ex.run; teardown } # let: lazy, memoized per example let(:user) { User.new(name: 'Eric') } let!(:post) { Post.create!(user: user) } # let! is eager — runs before each example # subject: the thing under test subject { described_class.new(name: 'Eric') } # described_class → the class in RSpec.describe
# Equality expect(x).to eq(5) # == expect(x).to eql(5) # .eql? expect(x).to equal(y) # same object expect(x).to be(nil) # identity # Truthiness expect(x).to be_truthy expect(x).to be_falsy expect(x).to be_nil # Comparison expect(x).to be > 5 expect(x).to be_between(1, 10) expect(x).to be_within(0.1).of(3.14) # Negation expect(x).not_to eq(5) expect(x).to_not be_nil
# Collections expect(arr).to include(3) expect(arr).to include(1, 2) expect(arr).to contain_exactly(1, 2, 3) # any order expect(arr).to match_array([3, 1, 2]) # any order expect(arr).to have_attributes(size: 3) expect(arr).to all(be > 0) expect(hash).to include(name: 'Eric') # Type expect(x).to be_a(String) expect(x).to be_an_instance_of(Hash) expect(x).to be_kind_of(Numeric) # Dynamic predicate matchers expect(user).to be_admin # user.admin? expect(str).to be_empty # str.empty? expect(obj).to have_key(:name) # obj.has_key?
# Basic double (strict by default in RSpec 3) dbl = double('User', name: 'Eric', age: 30) dbl.name # 'Eric' # Instance double (verifies against real class) dbl = instance_double(User, name: 'Eric') # Class double dbl = class_double(User) # Stub a method allow(user).to receive(:name).and_return('Eric') allow(user).to receive(:save).and_return(false) allow(User).to receive(:find).and_return(user) # Raise from stub allow(svc).to receive(:call).and_raise(RuntimeError)
# expect call to happen (mock) expect(mailer).to receive(:deliver_now) expect(user).to receive(:save).once expect(obj).to receive(:log).at_least(2).times expect(obj).to receive(:call).with('arg') expect(obj).not_to receive(:delete) # Spy (stub first, assert after) spy = spy('mailer') allow(spy).to receive(:deliver) # ... exercise code ... expect(spy).to have_received(:deliver).once # with matchers expect(obj).to receive(:go) .with(hash_including(name: 'Eric'))
# Exceptions expect { risky }.to raise_error expect { risky }.to raise_error(ArgumentError) expect { risky }.to raise_error(/bad input/) expect { safe }.not_to raise_error # Change expect { User.create!(name: 'x') } .to change(User, :count).by(1) expect { user.update!(age: 31) } .to change { user.age }.from(30).to(31) expect { action }.to change(Post, :count).by_at_least(1) # Output expect { puts 'hi' }.to output("hi\n").to_stdout
# Define shared examples RSpec.shared_examples 'a serializable object' do it { expect(subject).to respond_to(:to_json) } it { expect(subject).to respond_to(:as_json) } end describe User do it_behaves_like 'a serializable object' it_behaves_like 'a serializable object' do let(:subject) { User.new } end end # Pending / skip it 'is todo' # pending (no block) xit 'skip this' do ...end # skip it 'skip', skip: 'reason' do ...end
RSpec.describe 'POST /api/v1/users', type: :request do let(:valid_params) { { user: { name: 'Eric', email: 'e@x.com' } } } it 'creates a user' do post '/api/v1/users', params: valid_params, as: :json expect(response).to have_http_status(:created) expect(JSON.parse(response.body)['name']).to eq('Eric') end it 'returns errors on invalid data' do post '/api/v1/users', params: { user: { name: '' } }, as: :json expect(response).to have_http_status(:unprocessable_entity) expect(JSON.parse(response.body)).to include('errors') end end
| Matcher | Tests |
|---|---|
eq(val) | Value equality == |
be_nil / be_truthy / be_falsy | Nil / truthiness |
include(x) | Array/string/hash contains |
match_array([…]) | Same elements, any order |
contain_exactly(…) | Exact elements, any order |
have_attributes(k: v) | Object attribute values |
raise_error(Cls) | Exception raised |
change(obj, :attr).by(n) | Attribute changes by n |
be_a(Klass) | Type check (is_a?) |
respond_to(:method) | Method defined |
have_http_status | Rails response status |
be_* (dynamic) | Calls *? predicate method |