Friday, September 27, 2013

Magic: strings and regular expressions in ruby Cucumber tables


I stumbled on an interesting trick that is worth documenting

Say you have a Cucumber Scenario Outline like so:

 Scenario Outline: Foo and bar
    When I click < foo >
    Then < bar > should appear in the page
  Examples:
    | foo        | bar                |
    | X          | =String or regex   |
    | Y          | ===String or regex |
    | Z          |  String or regex   |

and we use it like so:

Then(/^(.+) should appear in the page$/) do |bar|
  on(FooPage).page_contents.should match Regexp.new(bar)
end 

but this won't work, because the case with "=" will pass by accident if page_contents erroneously contains "===" instead.  And case Z would always pass no matter how many '=' characters are in the page.  (That's clear, right?  Say something in the comments if that doesn't make sense to you.)

Also, I really need to check for a leading space for case Z, so what I think I need is not a string but a regular expression, so those anchor carets below should work, right?

  Examples:
    | foo        | bar                 |
    | X          | ^=String or regex   |
    | Y          | ^===String or regex |
    | Z          | ^ String or regex   |

That seems reasonable.  I've made sure that the values for 'bar' are interpreted as regexes, so this should Just Work...

What I discovered is that the values for 'bar' are actually magically turned into *escaped* regexes, so what ends up being compared to page_contents is actually:

/\^\=String or regex/
/\^\=\=\=String or regex/
/\^ String or regex/


and that is no help at all.  Even though they are regexes, the anchor characters are being escaped within the regex so the arguments continue to function as strings.  That is, what is being sent to RSpec as the argument to match is equivalent to the literal strings

"^=String or regex"
"^===String or regex"
"^ String or regex"

and of course that isn't what we want at all.

Here is what I finally figured out:

First we put the regexes that we want to use inside a pair of some character, in this case single quotes.  (And it works for leading spaces in strings, too!)

  Examples:
    | foo        | bar                   |
    | X          | '^=String or regex'   |
    | Y          | '^===String or regex' |
    | Z          | '^ String or regex'   |

Here's the tricky part.  Before we do the match in RSpec we strip the single quotes, and hey presto the result is an unescaped regex *not* a string:

Then(/^(.+) should appear in the page$/) do |bar|
  bar = bar.gsub(/'/, '')
  on(FooPage).page_contents.should match Regexp.new(bar)
end 

That is, what gets sent to RSpec as the argument to 'match' ends up being

/^=String or regex/
/^===String or regex/
/^ String or regex/

and is correctly interpreted by RSpec as a regex-- not as an escaped regex and not a string either.

Sometimes you can have a little too much magic.

2 comments:

Austin Ian said...
This comment has been removed by a blog administrator.
Win vinaya said...
This comment has been removed by a blog administrator.