Home
subscribe

L'etat, c'est moi

Mere Complexities sells the consulting and development services of me, Paul Wilson.

Conferences

Organising Scotland on Rails
Speaker, RailsConf Europe '08

Archive

Twitterlink available on Apple's Appstore

Twitterlink

Talking of Twitter clients, my first iPhone application – Twitterlink – is now available on the AppStore. It is a simple beast with one purpose: it enables posting shortened URLs from iPhone’s Safari web-browser to Twitter.

You can download Twitterlink from the app store here (app store link), or read a little more about it here.

It’s free for now. I will be putting the price up to (gasp) £0.56 ($0.99) soon.


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.

  1. 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.
  2. In our test code we are now monkey patching to, amongst other things, record the delegate passed to connectionWithRequest:delegate:.
  3. 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.
  4. 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.

Wot no mocks?

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.

Wot no RSPec?

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.

Wot no separation of concerns?

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.


Unit Testing iPhone Development with Rbiphonetest

I’ve been doing Phone development recently, and have a little application ready to send to the App Store. Although it’s not been long since the NDA was lifted, there is quite a lot of information out there. I’ve been mostly using:-

  • Apple’s own bundled documentation, which is very good.
  • the Pragmatic iPhone SDK Development, which is in Beta, and the associated forum.
  • StackOverflow, which has turned into an excellent source of information.
  • The official Apple iPhone Development forums (registration needed), but only a little for some reason.

One area not so well covered is test driving iPhone development; the guys writing the Prag book seem very old-school, and TDD is not mentioned. Dr Nic to the rescue. The great thing about Dr Nic is that he doesn’t mind getting his hands dirty, just trying things out and coming up with partial solutions for the community to work with. I think that’s brilliant. One of Dr Nic’s little brain children is Rbiphonetest, which enables (currently limited) unit testing of iPhone Objective-C in ruby. It’s work-in-progress, but quite nifty.

Here’s a little getting started tutorial. I’m not going to run through creating a whole app – just testing a bit of it. If you’re coding along I’m assuming you set up for iPhone development (Mac OS 10.5 Leopard with the latest iPhone SDK). I’m also assuming your Ruby gem version is up to date (sudo gem update—system), so the first thing you need to do is install the gem


    sudo gem install rbiphontest

Here I ran into a few problems installing on a new Mac: the gem dependencies weren’t properly sorted out, so I needed to install some other gems before successfully installing rbiphonetest; unfortunately I didn’t take a note, but I think they included hoe, and rubigen.

Were also going to want to run autotest. If you don’t already have it,


    sudo gem install ZenTest

Let’s do some TDD. Twitter clients are the new Flashlight apps, so let’s start one of those. Fire up XCode and create an iPhone OS project. (For the purposes of this example it doesn’t matter what type; a Utility app is as good as any). From the terminal go to your new application directory and initialise rbiphontest.


    rbiphontest .

This creates a few directories and scripts. It might also be a point where you encounter some dependency problems. If you do let me know and I’ll update this post; probably worthwhile dropping Dr Nic a line too.

Rbiphonetest only supports testing models just now, so let’s do that. We’ll need to store the user’s twitter account details, so let’s create a model for that. From the command line:


    script/generate model TwitterAccount

That’s created an Objective-C header and implementation file in the Classes directory, and a Ruby test in test. Flip into XCode and add TwitterAccount.h and TwitterAccount.m to the project. (Menu Project > New Group “Classes”. Right-click on “Classes” and choose Add > Existing Files. )

We can compile and run the test using


    rake

We get


1 tests, 0 assertions, 0 failures, 0 errors

Let’s do some TDD. XCode sucks, so I like to do most of the development in TextMate. Your mileage may vary, so open up “test/test_twitter_account.rb” in your own way.


    mate .

This is what we have:


require File.dirname(__FILE__) + '/test_helper'

require "TwitterAccount.bundle" 
OSX::ns_import :TwitterAccount

class TestTwitterAccount < Test::Unit::TestCase
  def test_twitter_account_class_exists
    OSX::TwitterAccount
  end
end 

The generated test just tests the compilation – fair enough. Let’s take a baby step: an account is going to need a username.


  def test_twitter_account_has_username
    testee = OSX::TwitterAccount.alloc
    testee.username = 'marvin'
    assert_equal "marvin", testee.username
  end

Run rake and we get:


SX::OCMessageSendException: Can't get Objective-C method 
signature for selector 'setUsername:'...
....
1 tests, 0 assertions, 0 failures, 1 errors

Yay, we have a failing test – we can write some production code.

TwitterAccount.h:


@interface TwitterAccount : NSObject {
@private
    NSString *username;
}
@property(retain, nonatomic) NSString *username;
@end

TwitterAccount.m:


@implementation TwitterAccount
@synthesize username;
@end

Run rake and


1 tests, 1 assertions, 0 failures, 0 errors

Notice that we don’t have an easily affordable way of testing the “retain” and “nonatomic” attributes of the property; I really don’t think that matters.

We need a password as well. We don’t want to have to keep on running rake, though. Open up a new command shell(or tab) and change into the project directory.


    autotest

Now everytime the a change is made to test, or code under test, any compilations required are performed and all the tests are run. With growl setup to give notifications, this is a very fluid way of TDDing Cocoa-Touch.

Ok, let’s add the test for the password.


def test_twitter_account_has_username_and_password
  testee = OSX::TwitterAccount.alloc
  testee.username = 'marvin'
  testee.password = 'secret'
  assert_equal "marvin", testee.username
  assert_equal "secret", testee.password
end

It fails – let’s make it pass.

TwitterAccount.h:


@interface TwitterAccount : NSObject {
@private
    NSString *username;
    NSString *password;
}
@property(retain, nonatomic) NSString *username;
@property(retain, nonatomic) NSString *password;
@end

TwitterAccount.m:


@implementation TwitterAccount
@synthesize username;
@synthesize password;
@end

That was easy the easy stuff. Let’s try something a bit more challenging. We want to be able to save and reload the account details.


def test_account_details_may_be_saved_and_reloaded
  testee = OSX::TwitterAccount.alloc
  testee.username = 'victoria'
  testee.password = 'secret'
  testee.save

  reloaded = OSX::TwitterAccount.alloc
  reloaded.load

  assert_equal "victoria", testee.username
  assert_equal "secret", testee.password       
end

Big step, but there’s a lot of boilerplate example for this kind of thing knocking around so let’s give it a go. We could add the following to TwitterAccount.m:


-(NSString*) accountPlistPath{
    NSString *userDir = 
    [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) 
            objectAtIndex:0];
    return [userDir stringByAppendingPathComponent:@"account.plist"];
}

-(void) save{
    NSString *path = [self accountPlistPath];
    NSDictionary *accountDetails = 
    [NSDictionary dictionaryWithObjectsAndKeys:self.username, 
        @"username", self.password, @"password", nil];
    BOOL written = [accountDetails writeToFile:path atomically:YES];
    NSLog(@"written: %d", written);

}

-(void) load{
    NSDictionary *accountDetails = 
   [[NSDictionary alloc] initWithContentsOfFile:[self accountPlistPath]];    
    self.username = [accountDetails objectForKey:@"username"];
    self.password = [accountDetails objectForKey:@"password"];
    [accountDetails release];
}

That passes but there are issues. For one thing it violates one of Mike Feathers’ unit testing rules: it touches the file system. In these cases, I tend to side with Terry Pratchett’s Lu-Tze:

Rules are there to make you think before you break them.

I have never had problems caused by the occasional unit test that hits the file system, so I don’t worry too much especially if the alternative is lots more code. On the other hand, this test is writing to the ~/Documents directory, which is too messy. Let’s trick that out.


class TestTwitterAccount < Test::Unit::TestCase
  def self.trick_account_plist_path
    @@account_plist_path||= Tempfile.new("accountPlistPath").path
  end 
  class OSX::TwitterAccount
    def accountPlistPath
      TestTwitterAccount.trick_account_plist_path
    end    
  end

  .....

end

Now the accountPlistPath method is not at all under test; let’s fix that by aliasing the original and showing that it saves to the user documents directory.


class OSX::TwitterAccount
  objc_alias_method :original_accountPlistPath, :accountPlistPath
  def accountPlistPath
    TestTwitterAccount.trick_account_plist_path
  end    
end

......

def test_account_plist_is_saved_to_user_document_directory
   assert_equal File.expand_path("~/Documents/account.plist"), 
   @testee.original_accountPlistPath
end

There is another test to write: let’s make sure we can handle having no previously saved preferences file.


def test_username_and_password_are_initially_nil_if_not_previously_saved
 File.delete TestTwitterAccount.trick_account_plist_path
 testee = OSX::TwitterAccount.alloc
 testee.load
 assert_equal nil, testee.username
 assert_equal nil, testee.password
end

The alloc and load is looking a bit cumbersome. Let’s add a static factory method.


  def setup
    FileUtils.rm_f TestTwitterAccount.trick_account_plist_path 
    @testee = OSX::TwitterAccount.loadFromPlist
  end

and


+(id)loadFromPlist{
    TwitterAccount *result = [[TwitterAccount alloc]autorelease];
    [result load];
    return result;
}

Now let’s tidy up the test a bit.


require File.dirname(__FILE__) + '/test_helper'
require 'tempfile'
require 'fileutils'

require "TwitterAccount.bundle" 
OSX::ns_import :TwitterAccount

class OSX::TwitterAccount
  objc_alias_method :original_accountPlistPath, :accountPlistPath
  def accountPlistPath
    TestTwitterAccount.trick_account_plist_path
  end    
end

class TestTwitterAccount < Test::Unit::TestCase
  def self.trick_account_plist_path
     @@account_plist_path||= Tempfile.new("accountPlistPath").path
  end 

  def setup
    FileUtils.rm_f TestTwitterAccount.trick_account_plist_path 
    @testee = OSX::TwitterAccount.loadFromPlist
  end

  def test_account_plist_is_saved_to_user_document_directory
     assert_equal File.expand_path("~/Documents/account.plist"), 
    @testee.original_accountPlistPath
  end

  def test_twitter_account_has_username_and_password
    @testee.username = 'marvin'
    @testee.password = 'tellnot'
    assert_equal "marvin", @testee.username
    assert_equal "tellnot", @testee.password
  end

  def test_account_details_may_be_saved_and_reloaded
    @testee.username = 'victoria'
    @testee.password = 'secret'
    @testee.save

    reloaded = OSX::TwitterAccount.alloc
    reloaded.load

    assert_equal "victoria", reloaded.username
    assert_equal "secret", reloaded.password       
  end

  def test_username_and_password_are_initially_nil_if_not_previously_saved
    assert_equal nil, @testee.username
    assert_equal nil, @testee.password
  end
end

That’s good enough for now. I’m beginning to wonder whether the persistence logic should be in the same place as the domain logic: for now I think it should; it’s under test so we can refactor with confidence should things get more complex later.

Coming soon:

  • unit testing sending a tweet to Twitter
  • stretching Rbiphonetest to test interaction between our presentation code and UIKit classes.
  • some Rbiphonetest gotchas

Voice recognition

Scotty

I see from Gruber that there’s another voice recognition app out for the iPhone. Why? Do people actually want or use this stuff? I mean apart from playing around or edge-cases such as when driving (where, incidentally, hands free operation is still very dangerous).

Developing voice-recognition systems is a symptom of over exposure to Star Trek at a vulnerable age. Voice commands to computers works well on film and TV for dramatic reasons. It’s the same reason TV characters read letters out loud, and on-screen dialogue boxes use HUGE fonts. A dramatic on-screen device does not make for a good user interface design. There are three reasons why voice-recognition input is a poor choice for computing, particularly mobile-computing:

  1. Everyone can hear you.
  2. There are perfectly good alternatives, like keyboards and touchscreens.
  3. EVERYONE IN THE DAMN TRAIN CARRIAGE CAN HEAR YOU. And if the train’s delayed by snow they’re going to get annoyed, and possibly gang-up and kill you. It will be like the Orient Express all over again, except without Peter Ustinov.

I leave you with a link to this comic, on the subject, from Joy of Tech. You’ve been warned.