We’re cross-posting this blog from Nate’s personal blog post on the topic.
I have been doing a lot of unit testing in Objective-C lately, mostly for the Socialize project (https://github.com/socialize/socialize-sdk-ios). In general, i’ve found that more testable code is usually better code, but this is not always the case. For example, sometimes i find myself leaning toward instance methods when a class method would have been the right choice. Or sometimes i will expose a class parameter just so i can override and/or mock it.
I have built a general solution for mocking class methods which i think may be helpful. It is an extension to OCMock which allows you to use all of the instance-level mocking on global classes or class objects.
For example, let’s say you wanted to test text message functionality on the iPhone simulator. There is not a very direct way to do this, since text message functionality is disabled on the simulator. One solution would be to expose an instance or class method on the class that uses MFMessageComposeViewController. This would just be a wrapper around [MFMessageComposeViewController canSendText]. You would then override the wrapper during testing.
While this is ok, but it does feel a little invasive and leads to unnecessary and even repeated code being generated. The following sample illustrates what i think might be a better solution to the problem:
This is a better simulation of the actual condition because it does not require modifying the code which uses the composer. I’ve set this up in the Socialize project, and it has proven quite useful. If you just want to check out the code, i’ve added it to our OCMock fork. The specific commit is a1f59f50b.
The way it works is by changing the metaclass of the target class (the one you’d like to be mocked). From then on, any calls to the class object will be sent to a shared OCMock object. The OCMock object is a mock of the classes metaclass. I gave details on how to make a metaclass mock in an earlier post. The shared class mocks are stored in a shared dictionary, and the ‘isa’ of the original class is modified to the ClassMockForwarder class, which handles forwarding to the shared mock for the duration of the mocking.
Since this is necessarily a global, shared state, I have configured my base GHUnit test classes to wipe out all class mocks whenever an exception is raised. This just prevents you from getting into a nasty situation where one test’s class mock state will affect another’s.
You can call anything that you would normally call on an OCMock object and its recorders. I don’t necessarily recommend you do this, but just to illustrate some possibilities, you could expect an allocation of a specific class and return a mock as follows:
autorelease cannot be overriden.
Raw init (with no parameters) cannot be stubbed, expected, or rejected because OCMockObject implements raw init. I can think of at least one approach to fixing this but it is sort of complex and I haven’t needed it yet.
There are some utilities as well. For example, to access the shared mock, you can use the +[NSObject classMock] method after calling startMockingClass. Or if you require access to the original class during mocking (say, to send a real message), you can access it with the +[NSObject origClass] method.
Let me know if you can make some use of this, or if you have any suggestions. It’s a bit patchy, but i am ok with that if the end result is to simplify the testing process.