What?

In this short post I’d like to show how some of RSpec components (matchers and expectations) can be used for a greater good outside your tests. Like in your normal everyday scripts.

Why?

Trying to be expressive, concise and readable, RSpec have developed a great number of idioms, mechanisms and approaches. They are reliable, most of Rubyists familiar with them, and code becames pretty expressive.

While I don’t recommend trying this at home using the tricks shown below in your Big Enterprise Production core, you may like those things for short scripts, experiments, insights, and just for fun.

How?

Argument matchers for checking values

require 'rspec'

include RSpec::Mocks::ArgumentMatchers

hash_including(:foo) === {foo: 1}
# => true
hash_including(:foo) === {bar: 1}
# => false

Those argument matchers just define case equality operator, making themselves really good for (kind of) pattern matching. Basically, for case (it is “case equality operator”, all in all!) and grep:

require 'rspec'

include RSpec::Mocks::ArgumentMatchers

case arg
when hash_including(:foo)
# ...
when hash_excluding(:bar)
# ...
when array_including(1, 2, 3)
# ...
when duck_type(:to_xyz) # just an object having this particular method
# ...
#
# standard RSpec matchers also work,
# you just need to include RSpec::Matchers too,
# which can have consequences - see below.
when be_within(0.001).of(28.3)
# ...
when have_attributes(class: String, length: 5)
# remember standard RSpec matchers are composable:
when have_attributes(class: String, length: 4).or(start_with('f'))
# ...and so on...
end

# another usage:
[1, %w[a b c], :foo].grep(duck_type(:each))
# => [["a", "b", "c"]]

Partial stubs to alter behavior quickly

require 'net/http'
require 'rspec'

include RSpec::Mocks::ExampleMethods
RSpec::Mocks.setup # <= will not work without this line :(

allow(Net::HTTP).to receive(:get).and_return('foo')

Net::HTTP.get('https://google.com')
# => "foo"

allow_any_instance_of(String).to receive(:size).and_return(8)
'foo'.size
# => 8

RSpec::Mocks.space.reset_all
'foo'.size
# => 3

Probe for unknown code

Not that useful, but you can easily drop it into unknown code (especially if it wants something complex or sensible) and see what will be done with it:

def test(database)
  database.drop_everything_forever
end

d = double.as_null_object

test(d)

# Not that pretty... But works
p ::RSpec::Mocks.space.proxy_for(d)
  .instance_variable_get('@messages_received')
# => [[:drop_everything_forever, [], nil]]
#      method                   args block

expect as Ye Olde Good Assert

CAUTION: HIGHLY QUESTIONABLE!

include RSpec::Matchers

def my_method_vith_validations(str)
  expect(str).to have_attributes(size: 4)

  puts str
end

my_method_with_validations('test') # => ok
my_method_with_validations('foo')
# RSpec::Expectations::ExpectationNotMetError: expected "foo" to have attributes {:size => 4} but had attributes {:size => 3}
# Diff:
# @@ -1,2 +1,2 @@
# -:size => 4,
# +:size => 3,

The error is pretty informative. So, you can use it like “oldschool” assert (for checking pre-, post- and intermediate conditions), just don’t forget to read “Don’t do a mess” section!

Other unholy tricks

require 'rspec'
include RSpec::Mocks::ExampleMethods
RSpec::Mocks.setup

stub_const('WHATEVER', 8)
WHATEVER
# => 8
stub_const('WHATEVER', 9)
WHATEVER
# => 9
RSpec::Mocks.space.reset_all
WHATEVER
# NameError: uninitialized constant WHATEVER

Don’t do a mess

If you really happy about the tricks above and want to move them into real code, I’d really suggest to evaluate how strong is your willing to pollute your namespaces with RSpec stuff. For example, “innocent” line

include RSpec::Matchers

…will “enrich” your current context with ton of methods like eq, be all, and method_missing that would treat any be_wtf as conversion to matcher for checking whether it’s argument responds to wtf? method and returns truthy value.

Maybe more sane approach would including/extending necessary RSpec features into isolated module, like this:

module M
  extend RSpec::Matchers
  extend RSpec::Mocks::ArgumentMatchers
end

some_huge_json.grep(M.hash_including(:foo))

…or even stricter tricks with delegating only really necessary things:

module M
  module Internals
    extend RSpec::Matchers
    extend RSpec::Mocks::ArgumentMatchers
  end

  class << self
    extend Forwardable

    def internals
      Internals
    end

    # OK, it is officially cumbersome now... But still useful!

    def_delegator :internals, :hash_including
  end
end

M.hash_including(:foo)
# => #<RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher:0x9a2b440 @expected={:foo=>#<RSpec::Mocks::ArgumentMatchers::AnyArgMatcher:0x9995a58>}>
M.array_including(1)
# undefined method `array_including' for M:Module (NoMethodError)

But, all in all, for experimenting and prototyping, most of shown techinques are rather helpful than destructive. Just be cautious moving your experiments to production.

And be curious.

And be happy.