RSpec Syntax Refenrence

I often use RSpec as my testing library when I code in Ruby. The documentation is not that great so I often use code that I already wrote as syntax reference.

Here I am going to write RSpec::Core and RSpec::Expectations syntax so I can refer it in the future.

RSpec::Expectations

Most commonly used:

RSpec.describe 'Common, built-in expectation matchers' do
  example 'Equality' do
    expect('x'+'y').to     eq('xy')   # a == b
    expect('x'+'y').to     eql('xy')  # a.eql?(b)
    expect('x'+'y').not_to be('xy')   # a.equal?(b)
  end

  example 'Strings' do
    expect('abcd').to include('bc')
    expect('abcd').to start_with 'ab'
    expect('abcd').to end_with 'cd'
    expect('abcd').to match /[a-z]+/
  end

  example 'Collections' do
    expect([1, 2, 3]).to include(1, 3)
    expect([1, 2, 3]).to contain_exactly(3, 2, 1) # order not important
    expect({ a: 1, b: 2 }).to include(b: 2)
  end

  example 'Booleans and nil' do
    expect(true).to be true
    expect(false).to be false
    expect('abc').to be_truthy
    expect(nil).to be_falsey
    expect(nil).to be_nil
  end

  example 'Numeric' do
    expect(5).to be > 4
    expect(5).to be >= 4
    expect(5).to be < 6
    expect(5).to be <= 6
    expect(5).to be_between(4, 6).exclusive
    expect(5).to be_between(5, 6).inclusive
    expect(4.99).to be_within(0.02).of(5)
  end

  example 'Errors (exceptions)' do
    expect{ 5 / 0 }.to raise_error(ZeroDivisionError)
    expect{ 5 / 0 }.to raise_error("divided by 0")
    expect{ 5 / 0 }.to raise_error(ZeroDivisionError, "divided by 0")
  end
end

Predicate matchers
DSL for calling methods that 1.return a boolean value 2. have a name that ends with ?

RSpec.describe 'Predicate matchers' do
  example 'Array' do
    expect([]).to be_empty          # [].empty?
  end

  example 'Hash' do
    expect({a: 1}).to have_key(:a)   # {a: 1}.has_key?(:a)
    expect({a: 1}).to have_value(1)  # {a: 1}.has_value?(1)
  end

  example 'Object' do
    expect(5).not_to be_nil             # 'hi'.nil?
    expect(5).to be_instance_of Fixnum  # 5.instance_of?(Fixnum)
    expect(5).to be_kind_of Numeric     # 5.kind_of?(Numeric)
  end
end

Predicate matchers work on all objects, including custom classes.

class Widget
  attr_accessor :name, :cost

  def initialize(name, cost)
    @name = name
    @cost = cost
  end

  def has_cliche_name?
    ['Foo', 'Bar', 'Baz'].include?(@name)
  end

  def hacker?
    @cost == 1337
  end
end

RSpec.describe 'Predicate matchers' do
  example 'With a custom class' do
    widget = Widget.new('Foo', 1337)

    expect(widget).to have_cliche_name
    expect(widget).to be_hacker
    expect(widget).to be_a_hacker
    expect(widget).to be_an_hacker
  end
end

Advanced matchers

# Add one extra method to the Widget class above,
# for demonstrating change observation.
class Widget
  def fifty_percent_off!
    @cost /= 2
  end
end

RSpec.describe 'Advanced matchers' do
  example 'Change observation' do
    widget = Widget.new('Baz', 80)

    expect{ widget.has_cliche_name? }.not_to change(widget, :name)

    expect{ widget.fifty_percent_off! }.to change(widget, :cost) # 80 -> 40
    expect{ widget.fifty_percent_off! }.to change(widget, :cost).from(40).to(20)
    expect{ widget.fifty_percent_off! }.to change(widget, :cost).by(-10) # 20 -> 10
  end

  example 'Object attributes' do
    # The attributes are a hash of method names to
    # expected return values.
    expect('hi').to have_attributes(length: 2, upcase: 'HI')
  end

  example 'Yielding to blocks' do
    expect{ |b| 5.tap(&b) }.to yield_control
    expect{ |b| 5.tap(&b) }.to yield_with_args(5)
    expect{ |b| 5.tap(&b) }.to yield_with_args(Integer)
    expect{ |b| 5.tap(&b) }.not_to yield_with_no_args
    expect{ |b| 3.times(&b) }.to yield_successive_args(0, 1, 2)
  end

  example 'Output capture' do
    expect{ puts 'hi' }.to output("hi\n").to_stdout
    expect{ $stderr.puts 'hi' }.to output("hi\n").to_stderr
  end

  example 'Throws' do
    # Not to be confused with errors (exceptions).
    # Throw/catch is very rarely used.
    expect{ throw :foo, 5 }.to throw_symbol
    expect{ throw :foo, 5 }.to throw_symbol(:foo)
    expect{ throw :foo, 5 }.to throw_symbol(:foo, 5)
  end

  example 'All elements in a collection' do
    expect([3,5,7]).to all(be_odd)
    expect([3,5,7]).to all(be > 0)
  end

  example 'Compound matchers' do
    expect(5).to be_odd.and be > 0
    expect(5).to be_odd.or be_even

    # These are probably most useful in combination with
    # the `all` matcher (above). For example:
    expect([1,2,3]).to all(be_kind_of(Integer).and be > 0)
  end

  example 'Satisfy' do
    # Test against a custom predicate
    expect(9).to satisfy('be a multiple of 3'){ |x| x % 3 == 0 }
  end
end

RSpec::Core

Most Commonly used

RSpec.describe 'an array of European cities' do
  let(:cities_array) { [:Berlin, :Prague, :Warsaw] }

  it 'has three cities' do
    expect(cities_array.size).to eq(3)
  end

  context 'mutation' do
    after { expect(cities_array.size).not_to eq(3) }

    it 'can have cities added' do
      cities_array << :Budapest
      expect(cities_array).to eq([:Berlin, :Prague, :Warsaw, :Budapest])
    end

    it 'can have cities removed' do
      cities_array.pop
      expect(cities_array).to eq([:Berlin, :Prague])
    end
  end
end

Subject

The subject key word accesses the object under test. Recreated for every test.

RSpec.describe Array do
  it 'provides methods based on the `RSpec.describe` argument' do
    # described_class = Array
    expect(described_class).to be(Array)
    # subject = described_class.new
    expect(subject).to eq(Array.new)
    # is_expected = expect(subject)
    is_expected.to eq(Array.new)
  end

  context 'explicitly defined subject' do
    # subject can be manually defined
    subject { [1,2,3] }
    it 'is not empty' do
      is_expected.not_to be_empty
    end
  end

  context 'can be named' do
    # you can provide a name, just like `let`
    subject(:bananas) { [4,5,6] }
    it 'can be called by name' do
     expect(bananas.first).to eq(4)
    end
  end
end

Hooks

Run arbitrary code before and after each test or context.

RSpec.describe 'Hooks' do
  order = []

  before(:all) { order << 'before(:all)' }
  before       { order << 'before' }
  after        { order << 'after' }
  after(:all)  { order << 'after(:all)'; puts order }

  around do |test|
    order << 'around, pre'
    test.call
    order << 'around, post'
  end

  it 'runs first test' do
    order << 'first test'
  end

  it 'runs second test' do
    order << 'second test'
  end
end

Execution order for the tests above:

before(:all)
around, pre
before
first test
after
around, post
around, pre
before
second test
after
around, post
after(:all)

Skipping

Momentarily prevents tests from being run.

RSpec.describe 'Ways to skip tests' do
  it 'is skipped because it has no body'

  skip 'uses `skip` instead of `it`' do
  end

  xit 'uses `xit` instead of `it`' do
  end

  it 'has `skip` in the body' do
    skip
  end

  xcontext 'uses `xcontext` to skip a group of tests' do
    it 'wont run' do; end
    it 'wont run either' do; end
  end
end

Pending

Temporarily ignore failing tests.

RSpec.describe 'Ways to mark failing tests as "pending"' do
  pending 'has a failing expectation' do
    expect(1).to eq(2)
  end

  it 'has `pending` in the body' do
    pending('reason goes here')
    expect(1).to eq(2)
  end

  pending 'tells you if a pending test has been fixed' do
    # Pending tests are supposed to fail. This test passes,
    # so RSpec will give an error saying that this pending
    # test has been fixed.
    expect(2).to eq(2)
  end
end

Helper Methods
Define arbitrary methods for use within your tests.

RSpec.describe 'Defining methods' do
  def my_helper_method(name)
    "Hello #{name}, you just got helped!"
  end

  it 'uses my_helper_method' do
    message = my_helper_method('Susan')
    expect(message).to eq('Hello Susan, you just got helped!')
  end

  context 'within a context group' do
    it 'can still use my_helper_method' do
      message = my_helper_method('Tom')
      expect(message).to eq('Hello Tom, you just got helped!')
    end
  end
end

Shared examples

A reusable set of tests that can be included into other tests.

RSpec.shared_examples 'acts like non-nil array' do
  it 'has a size' do
    expect(subject.size).to be > 0
  end

  it 'has has non-nil values for each index' do
    subject.size.times do |index|
      expect(subject[index]).not_to be_nil
    end
  end
end

RSpec.describe 'A real array' do
  include_examples 'acts like non-nil array' do
    subject { ['zero', 'one', 'two'] }
  end
end

RSpec.describe 'A hash with integer keys' do
  include_examples 'acts like non-nil array' do
    subject { Hash[0 => 'zero', 1 => 'one', 2 => 'two'] }
  end
end

Shared context;

A reusable context setup (hooks, methods and lets) that can be included into other tests.

RSpec.shared_context 'test timing' do 
 around do |test|
  start_time = Time.now
  test.call
  end_time = Time.now
  puts "Test ran in #{end_time - start_time} seconds
 end
end

RSpec.describe 'big array do 
  include_context 'test timing'
   it 'has many elements' do
     big_array = (1..1_000_000).to_a
     expect (big_array.size).to eq(1_000_000)
   end
end