How to mock singletons for testing in Cappuccino with OJTest

This post has been lurking for a while, but with the release of OJMoq 2, I decided I should probably just put this out there.  If you don't want to read about the motivation, skip to the sixth paragraph.

I've been on a testing kick for the past few months, motivated by the maturity of the OJTest library (thanks largely to the hard work of Derek Hammer).  OJTest has support for stubs, mocks and spies, which turns out to be very helpful when testing Cappuccino applications.  But one of the issues that I quickly ran into was writing tests around classes that make calls to Cappuccino classes that use default singletons.  For example, many parts of the Cappuccino code will use [CPNotificationCenter defaultCenter] for notifications or [CPBundle mainBundle] to bring in resources/read properties from an Info.plist file.  

So say you have a CPDocument subclass, called SCDocument, that you are writing tests for.  In a unit test, you may just instantiate SCDocument manually and exercise methods for it, but let's say you are being a bit more integrative in your tests and you want to instantiate your document the way Cappuccino normally would - by calling [[CPDocumentController sharedDocumentController] newDocument:self].  You'll realize that CPDocumentController looks to [CPBundle mainBundle] to find the document type to instantiate.  But if you don't have an Info.plist in your Tests directory, this fails.  So how can you tell CPDocumentController the class to instantiate?

One common way people work around problems like this is by using dependency injection.  To do this with the above example, you would first change CPDocumentController to accept a bundle and change all the calls to [[CPBundle mainBundle] call] to be [injectedBundle call].  Then, in your test, you would create a stub called stubBundle that returns the proper document type when you call [[stubBundle infoDictionary] objectForType:@"CPBundleDocumentTypes"].  Then you would inject this stub into CPDocumentController by calling [[CPDocumentController sharedController] setInjectedBundle:stubBundle].  

Simple, right? I didn't think so either.  When you add to this the fact that you would need to refactor a significant portion of Cappuccino to make all the classes accept dependencies, and that you would be making the design of Cappuccino's classes be based heavily on your testing needs, which is arguably bad, and that you would probably be making the API diverge farther away from Cocoa, you would end up with a mess quite a bit more messy than this sentence.

So, I decided to this another way.  Turns out in Cappuccino, you can treat class names like any other declared variable.  Since Cappuccino classes are also objects that understand selectors, you can really just stub out method calls to the class itself and simply reassign the global variable referring to the class to point to your stub.  So, in the above example, instead of making a stub CPBundle instance, I do the following (note that this is code written for OJMoq 1 - in the newer version, I would probably need to make a stub of CPBundle and put in fake implementations of a few more methods):

Now, any classes within Cappuccino that refer to CPBundle will get my stub instead without me having to change anything about these classes.  And once my test is done, the tearDown makes sure everything is back to normal.  

Posted