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