RSpec sugar to DRY your specs
Switch branches/tags
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.
lib Rubocop/Ruby versions Jun 28, 2018
.rspec Start proper gemification Aug 7, 2017
.rubocop.yml New rubocop Mar 9, 2018
.rubocop_todo.yml Rubocopping Aug 11, 2017
Gemfile Start proper gemification Aug 7, 2017
LICENSE.txt +be_json Mar 2, 2018
Rakefile Rakefile Aug 14, 2017
saharspec.gemspec Rubocop/Ruby versions Jun 28, 2018

Saharspec: Specs DRY as Sahara

Gem Version Build Status

saharspec is a set of additions to RSpec. It's name is a pun on Russian word "сахар" ("sahar", means "sugar") and Sahara desert. So, it is a set of RSpec sugar, to make your specs dry as a desert.


Install it as a usual gem saharspec with gem install or gem "saharspec" in :test group of your Gemfile.

Then, probably in your spec_helper.rb

require 'saharspec'
# or feature-by-feature
require 'saharspec/its/map'
# or some part of a library
require 'saharspec/its'



Just a random matchers I've found useful in my studies.

send_message(object, method) matcher

# before
it {
  expect(Net::HTTP).to receive(:get).with('').and_return('not this time')

# after
require 'saharspec/matchers/send_message'

it {
  expect { fetcher }.to send_message(Net::HTTP, :get).with('').returning('not this time')
# after + its_call
subject { fetcher }
its_call { send_message(Net::HTTP, :get).with('').returning('not this time') }

Note: there is reasons why it is not in rspec-mocks, though, not very persuative for me.

expect { block }.to ret(value) matcher

Checks whether #call-able subject (block, method, command object), when called, return value matching to expected.

Useful when this callable subject is your primary one:

# before: option 1. subject is value
subject { 2 + x }

context 'when numeric' do
  let(:x) { 3 }
  it { eq 5 } # DRY

context 'when incompatible' do
  let(:x) { '3' }
  it { expect { subject }.to raise_error } # not DRY

# option 2. subject is block
subject { -> {2 + x } }

context 'when incompatible' do
  let(:x) { '3' }
  it { raise_error } # DRY

context 'when numeric' do
  let(:x) { 3 }
  it { expect( eq 5 } # not DRY

# after
require 'saharspec/matchers/ret'

subject { -> { 2 + x } }

context 'when numeric' do
  let(:x) { 3 }
  it { ret 5 } # DRY: notice `ret`

context 'when incompatible' do
  let(:x) { '3' }
  it { raise_error } # DRY

Plays really well with its_call shown below.

be_json(value) and be_json_sym(value) matchers

Simple matcher to check if string is valid JSON and optionally if it matches to expected values:

expect('{}').to be_json # ok
expect('garbage').to be_json
# expected value to be a valid JSON string but failed: 765: unexpected token at 'garbage'

expect('{"foo": "bar"}').to be_json('foo' => 'bar') # ok

# be_json_sym is more convenient to check with hash keys, parses JSON to symbols
expect('{"foo": "bar"}').to be_json_sym(foo: 'bar')

# nested matchers work, too
expect('{"foo": [1, 2, 3]').to be_json_sym(foo: array_including(3))

# We need to go deeper!
expect(something_large).to be_json_sym(include(meta: include(next_page: Integer)))

eq_multiline(text) matcher

Dedicated to checking some multiline text generators.

# before: one option

  it { expect(generated_code).to eq("def method\n  a = @b**2\n  return a + @b\nend") }

# before: another option
  it {
    expect(generated_code).to eq(%{def method
  a = @b**2
  return a + @b

# after
require 'saharspec/matchers/eq_multiline'

  it {
    expect(generated_code).to eq_multiline(%{
      |def method
      |  a = @b**2
      |  return a + @b

(empty lines before/after are removed, text deindented up to | sign)

dont: matcher negation

Another (exprimental) attempt to get rid of define_negated_matcher. dont is not 100% grammatically correct, yet short and readable enought. It just negates attached matcher.

# before
RSpec.define_negated_matcher :not_change, :change

it { expect { code }.to do_stuff.and not_change(obj, :attr) }

# after: no `define_negated_matcher` needed
require 'saharspec/matchers/dont'

it { expect { code }.to do_stuff.and dont.change(obj, :attr) }


Notice: There are different opinions on usability/reasonability of its(:attribute) syntax, extracted from RSpec core and currently provided by rspec-its gem. Some find it (and a notion of description-less examples) bad practice. But if you are like me and love DRY-ness of it, probably you'll love those two ideas, taking its-syntax a bit further.


Like rspec/its, but for processing arrays:

subject {'ul#menu > li') }

# before
it { expect( all not_be_empty }

# after
require 'saharspec/its/map'

its_map(:text) { all not_be_empty }


Allows to DRY-ly refer to "block that calculates subject".

subject { some_operation_that_may_fail }

# before
context 'success' do
  it { eq 123 }

context 'fail' do
  it { expect { subject }.to raise_error(...) }

# after
require 'saharspec/its/block'

its_block { raise_error(...) }


Allows to DRY-ly test callable object with different arguments. Plays well with forementioned ret matcher.


# before
describe '#delete_at' do
  let(:array) { %i[a b c] }

  it { expect(array.delete_at(1) }.to eq :b }
  it { expect(array.delete_at(8) }.to eq nil }
  it { expect { array.delete_at(1) }.to change(array, :length).by(-1) }
  it { expect { array.delete_at(:b) }.to raise_error TypeError }

# after
require 'saharspec/its/call'

describe '#delete_at' do
  let(:array) { %i[a b c] }

  subject { array.method(:delete_at) }

  its_call(1) { ret :b }
  its_call(1) { change(array, :length).by(-1) }
  its_call(8) { ret nil }
  its_call(:b) { raise_error TypeError }

State & future

I use all of the components of the library on daily basis. Probably, I will extend it with other ideas and findings from time to time (next thing that needs gemification is WebMock DRY-er, allowing code like expect { code }.to request_webmock(url, params) instead of preparing stubs and then checking them). Stay tuned.


Victor Shepelev