For example, in expectations you create a mock at parse time, but you actually want the mock to be available at execution time.
puts "started"
new StartedProcess
end
end
Expectations do
expect SystemProcess.new.to.receive(:start) do |process|
process.start
end
end
In the above code you define what you expect when the file is parsed, but you actually want the process expectation to be set when the do block is executed. This can be (and is) achieved by using a recorder to record all the method calls on the process object. At execution time the method calls are played back and the initialized process object is yielded to the block.
The code for a recorder is actually quite trivial in Ruby.
attr_reader :subject
@subject = subject
end
method_stack.inject(subject) {|result, element| result.send element.first, *element.last }
end
@method_stack ||= []
end
method_stack << [sym, args]
self
end
end
Here's an example usage of a recorder.
puts "starting in "
sleep in_seconds
StartedProcess.new
end
end
puts "pausing in "
sleep in_seconds
PausedProcess.new
end
end
puts "stopping in "
sleep in_seconds
self
end
end
Recorder.new(SystemProcess.new).start(1).pause(2).stop(3).replay
# >> starting in 1
# >> pausing in 2
# >> stopping in 3
The only thing worth noting is that by using inject you can use a method chain that returns different objects. Traditional versions of a recorder that I've seen often assume that all the methods should be called on the subject. I prefer the version that allows for object creation within the fluent interface. In practice, that's exactly what was needed for recording and playing back Mocha's expectation setting methods.
This looks a lot like Methodphitamine, except that that allows blocks as arguments, and is designed for converting method stacks to procs:
ReplyDeletehttp://jicksta.com/posts/the-methodphitamine
Also, I've written something similar in JavaScript, which is a bit of a pain without method_missing:
http://jsclass.jcoglan.com/methodchain.html
It's a really powerful technique as it lets you store pieces of code and replay them later without explicitly writing any functions/blocks.
Isn't this what set_trace_func is used for? What are the benefits of using this instead of set_trace_func?
ReplyDeleteDo you use this pattern in tests in general?
ReplyDeleteI have yet to find a clean way to test that a block gets given to a method as Mocha doesn't have a way of 'expecting' a certain block. I can see that by extending this a little to store [method, args, block], you could record the method calls and then check that a certain method was called with a block that returns the correct results when called.
Example:
class Recorder
def method_stack
@method_stack ||= []
end
def method_missing(sym, *args, &block)
method_stack << [sym, args, block]
end
end
recorder = Recorder.new
recorder.foo { 'from the block' }
assert_equal 'from the block', recorder.method_stack.assoc(:foo)[2].call
Hi Jamie,
ReplyDeleteI haven't done anything like that, but it definitely looks interesting.
Cheers, Jay
Jay, how do you go about testing that a method gets called with a certain block, or a block that outputs a given result?
ReplyDeleteI'd love to see a clean solution to that problem. I generally get stuck testing DSL's that instance_eval a supplied block.
Jamie,
ReplyDeleteIt really depends on the context. I haven't found anything that 'generally' works.
Most often I end up writing some functional or higher level test.
Cheers, Jay
Jay,
ReplyDeleteThis Recorder in unit tests has really struck a nerve. It allows me to test things I could never test before.
Take the following example:
# Using a Recorder allows us to test the outcome of the block.
instance = A.new
recorder Factory.recorder_instance
instance.foo(recorder)
assert_equal 'dsl test', recorder[:method_that_takes_block].block.call
Implementation here: http://pastie.org/301666
I'm loving this as I have an especially complex DSL to test. I'm thinking of adding a sister to 'mock' and 'stub' in Mocha for 'recorder' as it is that useful. What are your thoughts?
Cheers,
Jamie
I think if you can find a way to make the syntax nice enough for James Mead, it's definitely a good idea.
ReplyDeleteYou're definitely not the only person who's ever wished they could test the result of a block.
Cheers, Jay
Simplified + gemified + tests + works with blocks
ReplyDeletehttps://github.com/grosser/method_call_recorder
Simplified + gemified + tests + works with blocks
ReplyDeletehttps://github.com/grosser/method_call_recorder