David Heinimeier Hansson recently chastised Rubyists for an increasing reliance on Dependency Injection. Here’s the article for those who haven’t seen it.
tl;dr Dependency Injection is mostly unneeded in dynamic languages. Rubyists who use DI are Java wannabes.
Now, I come from a strong statically typed background. I cut my teeth on C++ and most of my experience with Unit Testing has been with .NET & Java. Some might say I can write C# in any language. The disconnect between testability and readability in statically typed languages has always been a thorn in my side. It’s one of those things static TDD’ers don’t like to talk about. David’s post helped me grok some of the power of dynamic languages. It also got me thinking, if Objective-C is a dynamic language, how can I use that power to make my tests better without mucking up my API’s?
Take the following code (modified from DHH’s article)
@implementation Article - (void)publish { self.publishedAt = [NSDate date]; } @end
I would love to test this code with something like this:
- (void)testPublishedIsSet { NSDate *expectedPublishDate = [NSDate date]; Article *article = [[Article alloc] init]; [article publish]; // I wish I knew how to test you... STAssertEqualObjects(expectedPublishDate, article.publishedAt, @"Published at should be at now"); }
Unfortunatly, this doesn’t work. The question remains:
How do I test this code without injecting the value I want to test?
First, create a category for NSDate that returns an arbitrary NSDate value.
#import "NSDate+dateStub.h" @implementation NSDate (dateStub) + (NSDate*)dateStub { return [NSDate dateWithTimeIntervalSince1970:10000000]; // some arbitrary date value } @end
Next, swizzle [NSDate date] with [NSDate dateStub] so it returns a known value you can test against
- (void)testPublishedIsSetWithDateSwizzle { //Setup swizzle Method original,swizzled; original = class_getClassMethod([NSDate class], @selector(date)); swizzled = class_getClassMethod([NSDate class], @selector(dateStub)); method_exchangeImplementations(original, swizzled); // This now calls my dateStub NSDate *expectedPublishDate = [NSDate date]; Article *article = [[Article alloc] init]; [article publish]; // I pass now STAssertEqualObjects(expectedPublishDate, article.publishedAt, @"Published should be equal to date"); }
Done.
When my article calls [NSDate date], it’s actually returning my known value that I can run a test against. I’ve got testable code without having to pollute my API. Clarity plus testability. Dynamic languages and the Objective-C Runtime FTW!