I struggled using mocks in RubyCocoa. Here’s what I learned, using FlexMock.
I’m writing some tests to understand and explain the RubyCocoa interface to key-value observing. Key-value observing is yet another way for one object to observe another. (I’ll call these two objects the @watcher
and the @observed
.)
If I were describing a key-value observing test over the phone, I’d describe it as having four steps:
-
Set up the relationship between the two objects.
-
Change an attribute of @observed
.
-
During that change, expect some @watcher
method to be called. One of the arguments will be an NSDictionary
that contains the values before and after the change.
-
Also expect that the value really truly has been changed.
Here’s how I’d describe it in code:
def test_setting_in_ruby_style
newval = 5
during {
@observed.value = newval
}.behold! {
this_change(:old => @observed.value, :new => newval)
}
assert_equal(newval, @observed.value)
end
I have an idiosyncratic way of using mocks in Ruby. Notice the order in my English description above. I say what the test is doing before I say what happens during and after that action. But mock expectations have to be set up before the action. In Java (etc.) that requires steps 2 and 3 to be reversed. In Ruby, I can use blocks to put the steps in the more normal order. (You can see the definitions of during
and behold!
in the complete code.)
Since each of the tests sets up almost the same expectations, I hived that work off into this_change
:
def this_change(expected)
@watcher.should_receive(:observeValueForKeyPath_ofObject_change_context).
once.
with(’value‘,
@observed,
on { | actuals |
actuals[:old] == expected[:old] &&
actuals[:new] == expected[:new]
},
nil)
end
That’s a fairly typical use of FlexMock. I specify three arguments exactly, and I use code to check the remaining one. (There are components in the actuals NSDictionary
that I don’t care about.)
Now I have to start to do something special. Here’s setup
:
def setup
super
@observed = Observed.alloc.init
@watcher = flexmock(Watcher.alloc.init)
@observed.addObserver_forKeyPath_options_context(
@watcher, ‘value‘,
OSX::NSKeyValueObservingOptionNew | OSX::NSKeyValueObservingOptionOld,
nil)
end
Normally, an argument to flexmock
just provides a name for the mock, useful in debugging. However, key-value observing only works when the @watcher
object inherits from NSObject
. So I pass in an already-initialized object for FlexMock to “edit” and add mockish behavior to. It’s of class Watcher
, which inherits from NSObject
. But why not just use an NSObject
?
I could, except for a helpful feature of key-value observing. Before each call into the @watcher
, key-value observing checks if it responds to the method observeValueForKeyPath_ofObject_change_context
. If not, it raises an exception.
If I just passed in an NSObject
and gave FlexMock an expectation that it should_receive
an observeValueForKeyPath_ofObject_change_context
message, FlexMock would just attach that expectation to a list of expectations. It would not create any method of that name; rather, method_missing
traps such calls to the method and then checks off the appropriate expectation.
Therefore, I have to create a Watcher
class for no reason other than to contain the method:
class Watcher < OSX::NSObject
def observeValueForKeyPath_ofObject_change_context(
keyPath, object, change, context)
end
end
The method doesn’t have to actually do anything; it’s never called. Instead, FlexMock redefines it to trampoline a call to it over to message_missing
.
You can find the rest of the code here: mock-example.rb.
Hope this helps.