Project

saharspec

0.03
Repository is archived
Low commit activity in last 3 years
No release in over a year
Several additions for DRYer RSpec code
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Development

>= 0
~> 0.93
~> 0.9
>= 0
>= 3.7.0

Runtime

 Project Readme

Saharspec: Specs DRY as Sahara

NOTE: The gem was renamed to moarspec (for obvious reasons). Please update your gemfiles.


saharspec is a set of additions to RSpec.

Usage

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'

Parts

Matchers

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('http://google.com').and_return('not this time')
  fetcher
}

# after
require 'saharspec/matchers/send_message'

it {
  expect { fetcher }.to send_message(Net::HTTP, :get).with('http://google.com').returning('not this time')
}
# after + its_block
subject { fetcher }
its_block { is_expected.to send_message(Net::HTTP, :get).with('http://google.com').returning('not this time') }

Note: there are 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 { is_expected.to eq 5 } # DRY
end

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

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

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

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

# after
require 'saharspec/matchers/ret'

subject { -> { 2 + x } }

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

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

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
end})
  }

# after
require 'saharspec/matchers/eq_multiline'

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

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

dont: matcher negation

Allows to get rid of gazilliions 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) }

its-addons

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.

its_map

Like rspec/its, but for processing arrays:

subject { html_document.search('ul#menu > li') }

# before
it { expect(subject.map(&:text)).to all not_be_empty }

# after
require 'saharspec/its/map'

its_map(:text) { are_expected.to all not_be_empty }

its_block

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

subject { some_operation_that_may_fail }

# before
context 'success' do
  it { is_expected.to eq 123 }
end

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

# after
require 'saharspec/its/block'

its_block { is_expected.to raise_error(...) }

its_call

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

Before:

# 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 }
end

# after
require 'saharspec/its/call'

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

  subject { array.method(:delete_at) }

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

it_with/its_block_with

Allows to adjust let values inline with it definition. Especially useful when there are many cases to test, and each one requires only one simple test with simple let replacements.

# before
describe '#+' do
  subject { x + y }

  context 'with positive values' do
    let(:x) { 1 }
    let(:y) { 2 }

    it { is_expected.to eq 3 }
  end

  context 'with negative values' do
    # 4 more lines
  end

  context 'with non-numeric value' do
    # 4 more lines
  end
end

# after
describe '#+' do
  subject { x + y }

  it_with(x: 1, y: 2) { is_expected.to eq 3 }
  it_with(x: 1, y: -2) { is_expected.to eq -1 }
  its_block_with(x: 1, y: nil) { is_expected.to raise_error }
end

Example group helpers

instant_context

A shortcut for defining simple lets in the context description:

let(:user) { create(:user, role: role) }

# before: a lot of code to say simple things:

context 'when admin' do
 let(:role) { :admin }

 it { is_expected.to be_allowed }
end

context 'when user' do
 let(:role) { :user }

 it { is_expected.to be_denied }
end

# after

instant_context 'when admin', lets: {role: :admin} do
 it { is_expected.to be_allowed }
end

instant_context 'when user', lets: {role: :user} do
 it { is_expected.to be_denied }
end

# you can also give empty descriptions, then they would be auto-generated

# generates a context with description "role=:admin"
instant_context lets: {role: :admin} do
 it { is_expected.to be_allowed }
end

Note: when each context has only one example, it_with (see above) is probably more convenient. instant_context shines when the contexts are numerous and simple, but have several examples each.

Linting with RuboCop RSpec

rubocop-rspec fails to properly detect RSpec constructs that Saharspec defines (its_call, its_block, its_map). Make sure to use rubocop-rspec 2.0 or newer and add the following to your .rubocop.yml:

inherit_gem:
  saharspec: config/rubocop-rspec.yml

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.

Author

Victor Shepelev

License

MIT.