NOTE: The gem was renamed to moarspec (for obvious reasons). Please update your gemfiles.
saharspec is a set of additions to RSpec.
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.
# 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.
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
endPlays really well with its_call shown below.
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)))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)
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) }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 { 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 }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(...) }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 }
endAllows 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 }
endA 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 }
endNote: 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.
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.ymlI 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.
MIT.