L'etat, c'est moi
Mere Complexities sells the consulting and development services of me, Paul Wilson.
Conferences
Archive
iPhone TDD with Rbiphonetest II - Posting to Twitter
Earlier I wrote a brief tutorial about getting started with Rbiphonetest we test-drove saving and reloading Twitter account details. Now let’s go for something a bit more ambitious: sending a tweet.
We’ll start by checking a request is sent to the Twitter api url for tweeting.
def test_tweets_are_sent_to_the_correct_twitter_api_url
@testee.tweet "hello matey"
assert_equal "http://twitter.com/statuses/update.xml",OSX::NSURLConnection.last_request_url
end
We’ll need to trick-up NSURLConnection to record the last request url.
class OSX::NSURLConnection
class << self
attr_reader :last_request_url;
define_method 'connectionWithRequest:delegate:' do |request, delegate|
@last_request_url = request.URL.absoluteString
end
end
end
Something to note here we used define_method with the actual Objective-C method name, colons and all. There seems to be a RubyCocoa glitch that means that you need to use the exact method name, when monkey-patching methods. Usually RubyCocoa supports replacing colons in Objective-C method names with underscores: we would expect to be able to use connectionWithRequest_delegate as a Ruby equivalent, as indeed we can when calling the method. As ’:’ is not legal to use in a Ruby def statement, we are using define_method which is much more liberal.
Now for the minimum implementation to make the test pass.
-(void) tweet:(NSString*)message{
NSURLRequest *request = [NSURLRequest requestWithURL:
[NSURL URLWithString:@"http://twitter.com/statuses/update.xml"]];
[NSURLConnection connectionWithRequest:request delegate:nil];
}
That was easy, and we’re getting somewhere. Ok what else do we need to post a Tweet? Well it has to be a HTTP POST and contain the message in the parameter status.
def test_tweets_are_post_with_message_the_status_parameter
@testee.tweet "hello matey"
assert_equal "POST", OSX::NSURLConnection.last_request_method
assert_equal "status=hello matey", OSX::NSURLConnection.last_request_body
end
Let’s open up bit more of a window into the soul of NSURLConnection.
class OSX::NSData
def from_nil_terminated_str
i = 0
loop do
return bytes.bytestr(i) if bytes.int8_at(i) == 0
i+=1
end
end
end
class OSX::NSURLConnection
class << self
attr_reader :last_request_url, :last_request_method,
:last_request_body;
define_method 'connectionWithRequest:delegate:' do |request, delegate|
@last_request_url = request.URL.absoluteString
@last_request_method = request.HTTPMethod
@last_request_body = request.HTTPBody.from_nil_terminated_str if request.HTTPBody
end
end
end
We needed to do a bit of work converting the HTTPBody back to a string. Our solution isn’t bullet-proof as it’s UTF8 encoded, but as long as we stick to single byte UTF8 characters in our test data we’ll be fine.
In the implementation we need to use to a NSMutableURLRequest so we can set the HTTPMethod and HTTPBody properties.
-(void) tweet:(NSString*)message{
NSMutableURLRequest *request =
[NSMutableURLRequest requestWithURL:
[NSURL URLWithString:@"http://twitter.com/statuses/update.xml"]];
request.HTTPMethod = @"POST";
request.HTTPBody = [[NSString stringWithFormat:@"status=%@",message]
dataUsingEncoding:NSUTF8StringEncoding];
[NSURLConnection connectionWithRequest:request delegate:nil];
}
So who’s sending this Tweet? We need to authenticate ourselves; this is going to be done by an asynchronous callback. We’re going to need to do a bit more tricking things out: we’ll record the last delegate used by NSURLConnection.
class OSX::NSURLConnection
class << self
attr_reader :last_request_url, :last_request_method, :last_request_body, :delegate;
define_method 'connectionWithRequest:delegate:' do |request, delegate|
@last_request_url = request.URL.absoluteString
@last_request_method = request.HTTPMethod
@last_request_body = request.HTTPBody.from_nil_terminated_str if request.HTTPBody
@last_delegate = delegate
end
end
end
And we’ll need to use our own special authentication challenge to check what gets called.
class StubAuthenticationChallenge < OSX::NSURLAuthenticationChallenge
attr_reader :previousFailureCount, :sender
class StubSender
attr_reader :credential, :challenge, :cancelled_challenge
def useCredential_forAuthenticationChallenge credential, challenge
@credential = credential.retain.autorelease if credential
@challenge = challenge
end
def cancelAuthenticationChallenge challenge
@cancelled_challenge = challenge
end
end
def initialize
@previousFailureCount = 0
@sender = StubSender.new
end
def credential_used
@sender.credential
end
def challenge_used_with_credential
@sender.challenge
end
def cancelled?
@sender.cancelled_challenge
end
end
Note that our stub actually extends NSURLAuthenticationChallenge. We don’t need to do that our stub only returns pointers to Objective-C objects. If we have a method that returns a C structures – such as NSInteger, BOOL, and NSRange – we need to provide RubyCocoa a hint to what we are returning by overriding the class that defines that method: in this case previousFailureCount, which we will be using later, returns NSInteger.
Also worth noting is that we call retain on the credential in the StubSender_ method useCredential_forAuthenticationChallenge: this is to keep it around long enough for us to check its values.
def test_username_and_password_used_in_response_to_authentication_challenge
@testee.username = 'rita'
@testee.password = 'mysecret'
@testee.tweet "hi"
challenge = StubAuthenticationChallenge.alloc
assert OSX::NSURLConnection.last_delegate
OSX::NSURLConnection.last_delegate.connection_didReceiveAuthenticationChallenge nil,
challenge
assert_equal challenge, challenge.challenge_used_with_credential
assert challenge.credential_used
assert_equal "rita", challenge.credential_used.user
assert_equal "mysecret", challenge.credential_used.password
end
Confused? Maybe. Let’s walk through that a little.
- When our Objective-C code creates the connection asynchronously, we now need it to pass a ‘delegate’ to the connectionWithRequest:delegate: method on NSURLConnection; this delegate receives the asynchronous callbacks relaying the chaninging state of the connection.
- In our test code we are now monkey patching to, amongst other things, record the delegate passed to connectionWithRequest:delegate:.
- Our test can then call methods on the delegate to check the reaction of our code to callbacks: in this case the connection:didReceiveAuthenticationChallenge callback.
- We’ve stubbed out the StubAuthenticationChallenge, and record the challenge response to its sender.
(Note: as StubAuthenticationChallenge extends an Objective-C class we create using alloc instead of new: another RubyCocoa oddity.)
Let’s make that pass: we need a delegate to respond to the challenge.
@interface TweetDelegate : NSObject{
NSString *username;
NSString *password;
}
@property(nonatomic, retain) NSString* username;
@property(nonatomic, retain) NSString* password;
@end
@implementation TweetDelegate
@synthesize username;
@synthesize password;
+(id)tweetDelegateWithUsername:(NSString*) username andPassword:(NSString*) password{
TweetDelegate *result = [[TweetDelegate alloc] autorelease];
result.username = username;
result.password = password;
return result;
}
- (void)connection:(NSURLConnection *)connection
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
NSURLCredential *credential = [[NSURLCredential alloc]
initWithUser:self.username
password:self.password
persistence:NSURLCredentialPersistenceForSession];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
[credential release];
}
@end
And we’ll pass that delegate into the NSURLConnection creation.
-(void) tweet:(NSString*)message{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:
[NSURL URLWithString:@"http://twitter.com/statuses/update.xml"]];
request.HTTPMethod = @"POST";
request.HTTPBody = [[NSString stringWithFormat:@"status=%@",message]
dataUsingEncoding:NSUTF8StringEncoding];
[NSURLConnection connectionWithRequest:request delegate:
[TweetDelegate tweetDelegateWithUsername:self.username andPassword:self.password]];
}
Now we need to make sure that we only attempt the authentication once: if we have the wrong credentials the first time they are going to stay wrong no matter how many times we try to authenticate against them.
def test_only_attempts_to_authenticate_once_against_an_authentication_challenge
challenge = StubAuthenticationChallenge.alloc
challenge.previousFailureCount = 1
@testee.username = 'rita'
@testee.password = 'mysecret'
@testee.tweet "yo"
OSX::NSURLConnection.last_delegate.connection_didReceiveAuthenticationChallenge nil, challenge
assert challenge.credential_used.nil?
assert challenge.cancelled?
end
-(void)connection:(NSURLConnection *)connection
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
if ([challenge previousFailureCount] == 0){
NSURLCredential *credential = [[NSURLCredential alloc]
initWithUser:self.username
password:self.password
persistence:NSURLCredentialPersistenceForSession];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
[credential release];
}
else{
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
}
Now we’re tweeting!
Or are we? We don’t know as we’re using NSURLConnection asynchronously and we’re not hanging around long enough to find out what happened. Let’s get some feedback. We’ll start with an Objective-C protocol to be notified.
@protocol TweetObserver
-(void)tweetSucceeded;
-(void)tweetFailedWithError:(NSError*) error;
-(void)tweetFailedAuthentication;
@end
Ok, ok. I know that idiomatically we ought to call the protocaol TweetDelegate, but I just can’t bring myself to do that. Observer, Listener, even Callback – yes. But in what way is this a delegate? That’s right: in no way.
Now I’ve got that off my chest let’s stub that out and write the test to show us being notified of a successful tweet: we should get a connectionDidFinishLoading callback on success.
def test_should_be_notified_of_a_successful_tweet
@testee.tweet_andNotifyObserver "hey", @tweet_observer
OSX::NSURLConnection.last_delegate.connectionDidFinishLoading nil
assert @tweet_observer.tweet_succeeded
end
Ok, failing: we’ll change the tweet method to take the observer and pass it to the connection delegate.
-(void) tweet:(NSString*)message andNotifyObserver:(id<TweetObserver>) observer{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL
URLWithString:@"http://twitter.com/statuses/update.xml"]];
request.HTTPMethod = @"POST";
request.HTTPBody = [[NSString stringWithFormat:@"status=%@",message]
dataUsingEncoding:NSUTF8StringEncoding];
[NSURLConnection connectionWithRequest:request delegate:
[TweetDelegate tweetDelegateWithUsername:self.username
password:self.password andTweetObserver:observer]];
}
Now let’s wire up the observer to the delegate.
@interface TweetDelegate : NSObject{
NSString *username;
NSString *password;
id<TweetObserver> tweetObserver;
}
@property(nonatomic, retain) NSString* username;
@property(nonatomic, retain) NSString* password;
@property(nonatomic, retain) id<TweetObserver> tweetObserver;
@end
@implementation TweetDelegate
@synthesize username;
@synthesize password;
@synthesize tweetObserver;
+(id)tweetDelegateWithUsername:(NSString*)username password:(NSString*)password
andTweetObserver:(id<TweetObserver>) observer{
TweetDelegate *result = [[TweetDelegate alloc] autorelease];
result.username = username;
result.password = password;
result.tweetObserver = observer;
return result;
}
-(void)connectionDidFinishLoading:(NSURLConnection*)connection{
[self.tweetObserver tweetSucceeded];
}
.......
@end
And now to deal with failures.
def test_listener_notified_of_failure
error = OSX::NSError.alloc
@testee.tweet_andNotifyObserver "oi!", @tweet_observer
OSX::NSURLConnection.last_delegate.connection_didFailWithError nil, error
assert_equal error, @tweet_observer.tweet_error
assert !@tweet_observer.tweet_succeeded
end
def test_user_notified_of_authentication_failure
error = OSX::NSError.errorWithDomain_code_userInfo("NSURLErrorDomain",
OSX::NSURLErrorUserCancelledAuthentication, nil)
@testee.tweet_andNotifyObserver "hi!", @tweet_observer
OSX::NSURLConnection.last_delegate.connection_didFailWithError nil, error
assert @tweet_observer.tweet_failed_authentication
assert !@tweet_observer.tweet_error
assert !@tweet_observer.tweet_succeeded
end
Impementation:
@implementation TweetDelegate
.....
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
if ([error code] == NSURLErrorUserCancelledAuthentication){
[self.tweetObserver tweetFailedAuthentication];
}else{
[self.tweetObserver tweetFailedWithError:error];
}
}
@end
That’s about it. We’ve now test driven a functional increment: the ability to send a tweet. We’re not really done-done, though: there are still a few questions hanging around in the ether.

So we’ve gone to the trouble of hand-rolling all this stubbing around NSURLConnection and friends: why not use one of the popular Ruby mock-object framework, like FlexMock or Mocha? While I’m not keen on mocks, this isn’t the primary reason. After all we’ve ended up writing mostly interaction tests against NSURLConnection and friends, and Flexmock, at least, is somewhat agnostic about whether it is a mocking or a stubbing framework.
We mainly have not used a mocking framework in order to limit the number of moving parts in the example. This is experimental stuff: it is challenging enough to track down anomalies to coding mistakes, misunderstandings, or RubyCocoa glitches; throwing another framework, particularly one with the potential to react in interesting and unexpected ways with RubyCocoa, would make things even more difficult.
Consider also that the stubbing code that we have written is generally useful: it has the potential to be extracted into a library (gem) to be reused in different projects.

The mainstream of Ruby, particularly Rails, development has gone all BDD these days and RBIPhonetest does support RSpec. We have used TestUnit because I’m old-school. Expect to see some Shoulda in later posts, however.

If you’ve been following closely you may have noticed that we’ve added the functionality to send a tweet, with all that HTTP shenanigans, to the same class responsible for persisting the account details. Both the test and the productions code suffer from an unfortunate mix of concerns. But do not despair. Remember the old XP maxim:
Make it work, then make it right
In the next thrilling episode we’ll refactor the production code, supported by the tests; we’ll also refactor the tests, supported by the production code.