{"id":14991261,"url":"https://github.com/zverok/saharspec","last_synced_at":"2025-04-09T12:06:54.997Z","repository":{"id":15852397,"uuid":"78887601","full_name":"zverok/saharspec","owner":"zverok","description":"RSpec sugar to DRY your specs","archived":false,"fork":false,"pushed_at":"2023-11-26T15:21:31.000Z","size":94,"stargazers_count":74,"open_issues_count":2,"forks_count":6,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-02T09:11:15.362Z","etag":null,"topics":["rspec"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zverok.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-01-13T21:21:51.000Z","updated_at":"2025-03-27T23:52:25.000Z","dependencies_parsed_at":"2024-06-19T00:16:16.091Z","dependency_job_id":"93e86395-3122-4e8c-b849-c837f38ddad2","html_url":"https://github.com/zverok/saharspec","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fsaharspec","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fsaharspec/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fsaharspec/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fsaharspec/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zverok","download_url":"https://codeload.github.com/zverok/saharspec/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248036063,"owners_count":21037092,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["rspec"],"created_at":"2024-09-24T14:22:02.666Z","updated_at":"2025-04-09T12:06:54.973Z","avatar_url":"https://github.com/zverok.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Saharspec: Specs DRY as Sahara\n\n[![Gem Version](https://badge.fury.io/rb/saharspec.svg)](http://badge.fury.io/rb/saharspec)\n[![Build Status](https://travis-ci.org/zverok/saharspec.svg?branch=master)](https://travis-ci.org/zverok/saharspec)\n\n**saharspec** is a set of additions to RSpec. It's name is a pun on Russian word \"сахар\"\n(\"sahar\", means \"sugar\") and Sahara desert. So, it is a set of RSpec sugar, to make your\nspecs dry as a desert.\n\n## Usage\n\nInstall it as a usual gem `saharspec` with `gem install` or `gem \"saharspec\"` in `:test` group of\nyour `Gemfile`.\n\nThen, probably in your `spec_helper.rb`\n\n```ruby\nrequire 'saharspec'\n# or feature-by-feature\nrequire 'saharspec/its/map'\n# or some part of a library\nrequire 'saharspec/its'\n```\n\n## Parts\n\n### Matchers\n\nJust a random matchers I've found useful in my studies.\n\n#### `send_message(object, method)` matcher\n\n```ruby\n# before\nit {\n  expect(Net::HTTP).to receive(:get).with('http://google.com').and_return('not this time')\n  fetcher\n}\n\n# after\nrequire 'saharspec/matchers/send_message'\n\nit {\n  expect { fetcher }.to send_message(Net::HTTP, :get).with('http://google.com').returning('not this time')\n}\n# after + its_block\nsubject { fetcher }\nits_block { is_expected.to send_message(Net::HTTP, :get).with('http://google.com').returning('not this time') }\n```\n\nNote: there is [reasons](https://github.com/rspec/rspec-expectations/issues/934) why it is not in rspec-mocks, though, not very persuative for\nme.\n\n#### `expect { block }.to ret(value)` matcher\n\nChecks whether `#call`-able subject (block, method, command object), when called, return value matching\nto expected.\n\nUseful when this callable subject is your primary one:\n\n```ruby\n# before: option 1. subject is value\nsubject { 2 + x }\n\ncontext 'when numeric' do\n  let(:x) { 3 }\n  it { is_expected.to eq 5 } # DRY\nend\n\ncontext 'when incompatible' do\n  let(:x) { '3' }\n  it { expect { subject }.to raise_error } # not DRY\nend\n\n# option 2. subject is block\nsubject { -\u003e { 2 + x } }\n\ncontext 'when numeric' do\n  let(:x) { 3 }\n  it { expect(subject.call).to eq 5 } # not DRY\nend\n\ncontext 'when incompatible' do\n  let(:x) { '3' }\n  it { is_expected.to raise_error } # DRY\nend\n\n# after\nrequire 'saharspec/matchers/ret'\n\nsubject { -\u003e { 2 + x } }\n\ncontext 'when numeric' do\n  let(:x) { 3 }\n  it { is_expected.to ret 5 } # DRY: notice `ret`\nend\n\ncontext 'when incompatible' do\n  let(:x) { '3' }\n  it { is_expected.to raise_error } # DRY\nend\n```\n\nPlays really well with `its_call` shown below.\n\n#### `be_json(value)` and `be_json_sym(value)` matchers\n\nSimple matcher to check if string is valid JSON and optionally if it matches to expected values:\n\n```ruby\nexpect('{}').to be_json # ok\nexpect('garbage').to be_json\n# expected value to be a valid JSON string but failed: 765: unexpected token at 'garbage'\n\nexpect('{\"foo\": \"bar\"}').to be_json('foo' =\u003e 'bar') # ok\n\n# be_json_sym is more convenient to check with hash keys, parses JSON to symbols\nexpect('{\"foo\": \"bar\"}').to be_json_sym(foo: 'bar')\n\n# nested matchers work, too\nexpect('{\"foo\": [1, 2, 3]').to be_json_sym(foo: array_including(3))\n\n# We need to go deeper!\nexpect(something_large).to be_json_sym(include(meta: include(next_page: Integer)))\n```\n\n#### `eq_multiline(text)` matcher\n\nDedicated to checking some multiline text generators.\n\n```ruby\n# before: one option\n\n  it { expect(generated_code).to eq(\"def method\\n  a = @b**2\\n  return a + @b\\nend\") }\n\n# before: another option\n  it {\n    expect(generated_code).to eq(%{def method\n  a = @b**2\n  return a + @b\nend})\n  }\n\n# after\nrequire 'saharspec/matchers/eq_multiline'\n\n  it {\n    expect(generated_code).to eq_multiline(%{\n      |def method\n      |  a = @b**2\n      |  return a + @b\n      |end\n    })\n  }\n```\n(empty lines before/after are removed, text deindented up to `|` sign)\n\n### `dont`: matcher negation\n\nAllows to get rid of gazilliions of `define_negated_matcher`. `dont` is not 100% grammatically\ncorrect, yet short and readable enought. It just negates attached matcher.\n\n```ruby\n# before\nRSpec.define_negated_matcher :not_change, :change\n\nit { expect { code }.to do_stuff.and not_change(obj, :attr) }\n\n# after: no `define_negated_matcher` needed\nrequire 'saharspec/matchers/dont'\n\nit { expect { code }.to do_stuff.and dont.change(obj, :attr) }\n```\n\n### `its`-addons\n\n**Notice**: There are different opinions on usability/reasonability of `its(:attribute)` syntax,\nextracted from RSpec core and currently provided by [rspec-its](https://github.com/rspec/rspec-its)\ngem. Some find it (and a notion of description-less examples) bad practice. But if you are like me\nand love DRY-ness of it, probably you'll love those two ideas, taking `its`-syntax a bit further.\n\n#### `its_map`\n\nLike `rspec/its`, but for processing arrays:\n\n```ruby\nsubject { html_document.search('ul#menu \u003e li') }\n\n# before\nit { expect(subject.map(\u0026:text)).to all not_be_empty }\n\n# after\nrequire 'saharspec/its/map'\n\nits_map(:text) { are_expected.to all not_be_empty }\n```\n\n#### `its_block`\n\nAllows to DRY-ly refer to \"block that calculates subject\".\n\n```ruby\nsubject { some_operation_that_may_fail }\n\n# before\ncontext 'success' do\n  it { is_expected.to eq 123 }\nend\n\ncontext 'fail' do\n  it { expect { subject }.to raise_error(...) }\nend\n\n# after\nrequire 'saharspec/its/block'\n\nits_block { is_expected.to raise_error(...) }\n```\n\n#### `its_call`\n\nAllows to DRY-ly test callable object with different arguments. Plays well with forementioned `ret`\nmatcher.\n\nBefore:\n\n```ruby\n# before\ndescribe '#delete_at' do\n  let(:array) { %i[a b c] }\n\n  it { expect(array.delete_at(1) }.to eq :b }\n  it { expect(array.delete_at(8) }.to eq nil }\n  it { expect { array.delete_at(1) }.to change(array, :length).by(-1) }\n  it { expect { array.delete_at(:b) }.to raise_error TypeError }\nend\n\n# after\nrequire 'saharspec/its/call'\n\ndescribe '#delete_at' do\n  let(:array) { %i[a b c] }\n\n  subject { array.method(:delete_at) }\n\n  its_call(1) { is_expected.to ret :b }\n  its_call(1) { is_expected.to change(array, :length).by(-1) }\n  its_call(8) { is_expected.to ret nil }\n  its_call(:b) { is_expected.to raise_error TypeError }\nend\n```\n\n### Metadata handlers\n\n(Experimental.) Those aren't required by default, or by `require 'saharspec/metadata'`, you need to require each by its own. This is done to lessen the confusion if metadata processing isn't expected.\n\n#### `lets:`\n\nA shortcut for defining simple `let`s in the description\n\n```ruby\nlet(:user) { create(:user, role: role) }\n\n# before: a lot of code to say simple things:\n\ncontext 'when admin' do\n let(:role) { :admin }\n\n it { is_expected.to be_allowed }\nend\n\ncontext 'when user' do\n let(:role) { :user }\n\n it { is_expected.to be_denied }\nend\n\n# after\n\ncontext 'when admin', lets: {role: :admin} do\n it { is_expected.to be_allowed }\nend\n\ncontext 'when user', lets: {role: :user} do\n it { is_expected.to be_denied }\nend\n\n# you can also give empty descriptions, then they would be auto-generated\n\n# generates a context with description \"with role=:admin\"\ncontext '', lets: {role: :admin} do\n it { is_expected.to be_allowed }\nend\n```\n\n### Linting with RuboCop RSpec\n\n`rubocop-rspec` fails to properly detect RSpec constructs that Saharspec defines (`its_call`, `its_block`, `its_map`).\nMake sure to use `rubocop-rspec` 2.0 or newer and add the following to your `.rubocop.yml`:\n\n```yaml\ninherit_gem:\n  saharspec: config/rubocop-rspec.yml\n```\n\n## State \u0026 future\n\nI use all of the components of the library on daily basis. Probably, I will extend it with other\nideas and findings from time to time (next thing that needs gemification is WebMock DRY-er, allowing\ncode like `expect { code }.to request_webmock(url, params)` instead of preparing stubs and then\nchecking them). Stay tuned.\n\n## Author\n\n[Victor Shepelev](http://zverok.github.io/)\n\n## License\n\n[MIT](https://github.com/zverok/saharspec/blob/master/LICENSE.txt).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzverok%2Fsaharspec","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzverok%2Fsaharspec","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzverok%2Fsaharspec/lists"}