What?

RSpec API is constantly evolving towards as DRY and readable DSL as possible. Though, there are several things (tricks and additional methods) that could make your specs even DRYer.

Warning: While reading, you may feel a temptation to say something like “instead of using this, you’d better redesign your code for testability!” Let’s imagine it was already said, OK?

How?

Fancy subjects

subject statement is a really idiomatic addition to modern RSpec, which allows to unambiguosly say what are we testing, and simplify tests afterwards.

# Good and DRY:
subject { 'foo' }
it { is_expected.to eq 'foo' }

Using rspec-its (which was once in RSpec core, but extracted to separate gem in v3), we can do even more inventive checks:

# Still good and DRY:
its(:size) { is_expected.to eq(3) }

But this approach have some limitations.

subject as Array

If you want to check (= have a subject) an array of values, there is no idiomatic way to do its-checks on it:

# We need to repeat `subject` and have long "expect" argument :(
subject { %w[foo test shenanigans] }
it { expect(subject.map(&:size)).to include(3) }

WDYT about this trick?

subject { %w[foo test shenanigans] }
its_map(:size) { is_expected.to include(3) }

Implementation (assumes you’ve already required rspec/its and reuses its namespace):

module RSpec
  module Its
    def its_map(attribute, *options, &block)
      describe("map(&:#{attribute})") do
        let(:__its_map_subject) do
          attribute_chain = attribute.to_s.split('.').map(&:to_sym)
          attribute_chain.inject(subject) do |inner_subject, attr|
            inner_subject.map(&attr)
          end
        end

        def is_expected
          expect(__its_map_subject)
        end

        alias_method :are_expected, :is_expected

        options << {} unless options.last.is_a?(Hash)

        example(nil, *options, &block)
      end
    end
  end
end

Chains work also:

its_map(:'chars.first') { is_expected.to include('s') }

subject as a block

Imagine this code:

describe 'addition' do
  subject { 'foo' + other }

  context 'when compatible' do
    let(:other) { 'bar' }
    # nice & DRY!
    it { is_expected.to eq 'foobar' }
  end

  context 'when incompatible' do
    let(:other) { 5 }
    # subject is repeated :(
    it { expect { subject }.to raise_error(TypError) }
  end
end

Wouldn’t it look better this way? (Spoiler: for me, it would.)

subject { 'foo' + other }
# ...
context 'when incompatible' do
  let(:other) { 5 }
  its_call { is_expected.to raise_error(TypeError) } # OK, it's DRY too now!
end

Implementation (assumes you’ve already required rspec/its and reuses its namespace):

module RSpec
  module Its
    def its_call(*options, &block)
      describe("call") do
        let(:__call_subject) do
          -> { subject }
        end

        def is_expected
          expect(__call_subject)
        end

        example(nil, *options, &block)
      end
    end
  end
end

Works with tons of useful matchers:

subject { hunter.shoot_at(:lion) }
its_call { is_expected.to change(Lion, :count).by(-1) }

subject as a method

Imagine you need to test tons of return values of some relatively simple method, in different conditions it should return different results. How you do it?

it { expect(fetch(:age)).to eq 30 }
it { expect(fetch(:weight)).to eq 50 }
it { expect(fetch(:name)).to eq 'June' }
# .... do you see a pattern here?..

What about this? (Note, that it is just rspec + rspec/its, no additional code needed):

subject { ->(field) { fetch(field) } }

its([:age]) { is_expected.to eq 30 }
its([:weight]) { is_expected.to eq 50 }
its([:name]) { is_expected.to eq 'June' }

It “just works”, because the form its([arg]) calls [] on subject, and Ruby’s Proc defines [] as a synonym to .call!

Note: Another way to test the same subject is update its_call above to accept arguments, like its_call(:age) { is_expected.to eq 30 }.

Fun with matchers

Both ideas in this section are already considered and rejected by RSpec maintainers. But it doesn’t mean they are absolutely useless (TBH, I find them both much clearer than those preferred by maintainers).

Negations

RSpec allows logical matchers composition to some extent, with negations (.not_to), and .and/.or chaining. Unfortunately, currently there is no way to say “to this and not that” (because of long discussion of scope ambiguity).

Currently, this solution is considered standard:

RSpec.define_negated_matcher :not_to_change, :change

# ...

expect { action }.to change(Goods, :amount).by(1).and not_to_change(Money, :amount)

Most of the time it is good enough, though considered awkwardly complicated by some (including me, I saw codebases, where change matcher negations were defined in tens of files, with different names).

So, here is naive remedy:

require 'rspec/matchers'

module RSpec
  module Matchers
    module BuiltIn
      class Not < BaseMatcher
        def initialize(matcher)
          @matcher = matcher
        end

        def description
          "not #{@matcher.description}"
        end

        def match(_expected, actual)
          !@matcher.matches?(actual)
        end

        def supports_block_expectations?
          @matcher.supports_block_expectations?
        end
      end
    end

    module Composable
      def and_not(matcher)
        BuiltIn::Compound::And.new(self, BuiltIn::Not.new(matcher))
      end
    end
  end
end

Now you can just

expect { action }.to change(Goods, :amount).by(1).and_not change(Money, :amount)

Method expectations

Again, there is long-living (yet hard for me to follow) discussion why “expect some code to call some methods on some objects” is the only RSpec check that can’t be expressed in a single statement.

Currently, you can do:

# either
expect(obj).to receive(:method)
some_code

# or
obj = spy("Class")
some_code(obj)
expect(obj).to have_received(:method)

For me, neither look “atomic” enough. So, the solution:

RSpec::Matchers.define :send_message do |object, message|
  match do |block|
    allow(object).to receive(message)
      .tap { |m| m.with(*@with) if @with }
      .tap { |m| m.and_return(@return) if @return }
      .tap { |m| m.and_call_original if @call_original }

    block.call

    expect(object).to have_received(message)
      .tap { |m| m.with(*@with) if @with }
  end

  chain :with do |*with|
    @with = with
  end

  chain :returning do |returning|
    @return = returning
  end

  chain :calling_original do
    @call_original = true
  end

  supports_block_expectations
end

Now, you can just:

# arguments order like in change() matcher
expect { some_code }.to send_message(obj, :method).with(1, 2, 3).returning(5)

# works great with aforementioned its_call
subject { some_code }
it { is_expected.to send_message(obj, :method).with(1, 2, 3).returning(5) }

Nifty, ha?

Some final notes

I use all of code snippets shown above in my everyday work, and they show themselves as working and helpful. Though, it may be other way for you. In any case, notice that implementations are working, yet naive (or “naive, yet working” if you want). I gathered all of them in witty-named repo, but still haven’t made in proper gem with tests and docs and stuff. Though, you already can use it from GitHub (it has a proper gemspec), and say me what you think.

Just add this to your Gemfile (development group probably):

gem 'saharspec', git: 'https://github.com/zverok/saharspec.git'

Reddit discussion with pretty heated argument on one-line/DRY specs, and whether they are any good.