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.