Ruby idiom for running code twice ("in-between code")
In my Capybara+Webdriver AJAX tests, I see a pattern of code like this:
page.should have_selector('foo.bar > baz') # added dynamically by JS
visit current_page
page.should have_selector('foo.bar > baz') # still there after reload
I extracted this into a persist
helper function, which does
开发者_C百科def persist
yield
visit current_page
yield
end
Question: Is there a compact idiom to do the same thing inline, without a helper function?
The shortest I've been able to come up with is
2.times { |i|
page.should have_selector('foo.bar > baz')
visit current_page if i == 0
}
which is DRY but still ugly.
Edit: I think Mark's comment is quite right, and I'm sticking with my persist
helper for this particular use case. That said, there's several good (and interesting) ideas in the answers below.
Edit 2: In case anybody wants to copy my persist
example: With RSpec, it's useful to put a @__memoized = {}
after visit current_page
so as to refresh any lets holding nodes that will go outdated after the page reload (else you get an ObsoleteElementError
).
In my opinion, there's nothing wrong with repeating a line if it makes the code more readable. Your case seems to be a good example of such justified repetition :)
If you find yourself repeating the pattern a lot, you may want to extend Object
with something like
def should_still(predicate)
should predicate
yield
should predicate
end
then you can write compact statements like
page.should_still have_selector('foo.bar > baz') { visit current_page }
You can make a general-purpose version following the pattern of http://groups.google.com/group/comp.lang.ruby/browse_thread/thread/1052c289b22c60a5
class AroundWrapper
def initialize &block
(class << self
def self.outer &block
define_method :outer, &block
end
def self.inner &block
define_method :inner, &block
end
self
end).class_eval &block
end
end
def around &block
around_wrapper = AroundWrapper.new &block
around_wrapper.outer
around_wrapper.inner
around_wrapper.outer
end
Then, this:
around {
outer { puts "Hello" }
inner { puts "World" }
}
Will produce this output:
Hello
World
Hello
EDIT: Actually, now that I think of it, here's a much easier way that also reads quite nicely in usage:
def around(inner)
yield
inner.call
yield
end
around(lambda{puts "World"}) do
puts "Hello"
end
In this case, I would steer you away from "clever" solutions. Tests are supposed to be fairly procedural recipes, and if you abstract into something like your example, the test loses its readability. I can easily understand the intent and purpose of the first test, but the "condensed" version is as clear as mud.
In this case, I would encourage you to not necessarily feel like you need to avoid code duplication if it costs you clarity. If you still want to abstract it, I would recommend something like a helper, like
after_refresh { page.should have_selector("foo.bar > baz") }
Then,
def after_refresh(&block)
yield
visit current_page
yield
end
That keeps the test expressive, and lets you avoid duplication if you're testing a lot of these refresh cases.
精彩评论