Print Shortlink

TDD in iPhone applications

One aspect of development of apps for iOS devices, that has surprised me is developers ignoring the unit testing capabilities of Objective-C. Most of the developers who develop apps on iPhone ignore the testcase files that are created while creating new classes. Some peacefully uncheck the option of creating test-cases as well. This is not the case with Java/.NET/Ruby/Groovy application developers, however.

Objective-C applications can be developed using Test Driven Development like the other counterparts. You’ve SenTestingKit API and mock object frameworks like ocmock. You have the same assertXXX(in this case STAssertXXX functions) functions, setup, teardown etc., here as well. It just requires a discipline to use them.

We were playing with collection classes in Objective-C, so decided to implement a TicTacToe game on the iPhone. The TicTacToe game logic was developed in a class “TicTacToeEngine” using TDD and the UI was developed later. The TicTacToeEngineTests file(s) is shown below. The TicTacToeEngineTests.h gives you the list of tests in the file. It serves as a good documentation.

//TicTacToeEngineTests.h
#import <SenTestingKit/SenTestingKit.h>
#import "TicTacToeEngine.h"

@interface TicTacToeTests : SenTestCase

@property TicTacToeEngine* tictactoeEngine;

//test methods
-(void)testTicTacToeEngineNotNull;
-(void)testTicTacToeEngineInitializedWith9Squares;
-(void)testGameOverIsFalseInTheBeginning;
-(void)testInitialPlayerIsO;
-(void)testStartGameByPlacingOAtAPosition;
-(void)testPlayGameByPlacingXAtAPosition;
-(void)testCheckIfXIsGettingPlaced;
-(void)testCheckPlacePegAtAlreadyOccupiedPosition;
-(void)testPlayGameWithAlternatePlayersOccupyingPosition;
-(void)testGetUnoccupiedPosition;
-(void)testGameOverIfRows1And2And3Match;
-(void)testGameOverIfRows7And8And9Match;
-(void)testGameOverIfColumns1And4And7Match;
-(void)testGameOverIfColumns2And5And8Match;
-(void)testGameOverIfColumns3And6And9Match;
-(void)testGameOverIfDiagonals3And5And7Match;
-(void)testPlayAfterGameIsOver;
@end
//TicTacToeEngineTests.m

#import "TicTacToeEngineTests.h"
#import "TicTacToeEngine.h"

@implementation TicTacToeTests

@synthesize tictactoeEngine;
- (void)setUp
{
    [super setUp];
    tictactoeEngine = [[TicTacToeEngine alloc]init];
}

- (void)tearDown
{
    tictactoeEngine = nil;
    [super tearDown];
}
-(void)testPlayAfterGameIsOver{
    [tictactoeEngine start];
    [tictactoeEngine placePegAt:3];
    [tictactoeEngine placePegAt:6];
    [tictactoeEngine placePegAt:9];
    STAssertThrows([tictactoeEngine placePegAt:7], @"Game is already over");
}
-(void)testGameOverIfDiagonals3And5And7Match{
    [tictactoeEngine start];
    [tictactoeEngine placePegAt:2];
    [tictactoeEngine placePegAt:4];
    [tictactoeEngine placePegAt:6];
    STAssertTrue(tictactoeEngine.gameOver,@"Game is over");
    STAssertTrue([tictactoeEngine.winner isEqualToString:@"C"],@"Game is over");
}
-(void)testGameOverIfColumns3And6And9Match{
    [tictactoeEngine start];
    [tictactoeEngine placePegAt:3];
    [tictactoeEngine placePegAt:6];
    [tictactoeEngine placePegAt:9];
    STAssertTrue(tictactoeEngine.gameOver,@"Game is over");
    STAssertTrue([tictactoeEngine.winner isEqualToString:@"X"],@"Game is over");
}
-(void)testGameOverIfColumns2And5And8Match{
    [tictactoeEngine start];
    [tictactoeEngine placePegAt:2];
    [tictactoeEngine placePegAt:5];
    [tictactoeEngine placePegAt:8];
    STAssertTrue(tictactoeEngine.gameOver,@"Game is over");
    STAssertTrue([tictactoeEngine.winner isEqualToString:@"X"],@"Game is over");
}
-(void)testGameOverIfColumns1And4And7Match{
    [tictactoeEngine start];
    [tictactoeEngine placePegAt:2];
    [tictactoeEngine placePegAt:5];
    [tictactoeEngine placePegAt:6];
    STAssertTrue(tictactoeEngine.gameOver,@"Game is over");
    STAssertTrue([tictactoeEngine.winner isEqualToString:@"C"],@"Game is over");
}
-(void)testGameOverIfRows7And8And9Match{
    [tictactoeEngine start];//1
    [tictactoeEngine placePegAt:7];
    [tictactoeEngine placePegAt:3];
    [tictactoeEngine placePegAt:8];
    [tictactoeEngine placePegAt:9];
    STAssertTrue(tictactoeEngine.gameOver,@"Game is over");
    STAssertTrue([tictactoeEngine.winner isEqualToString:@"X"],@"Game is over");
}
-(void)testGameOverIfRows1And2And3Match{
    [tictactoeEngine start];
    [tictactoeEngine placePegAt:4];
    [tictactoeEngine placePegAt:6];
    STAssertTrue(tictactoeEngine.gameOver,@"Game is over");
    STAssertTrue([tictactoeEngine.winner isEqualToString:@"C"],@"Game is over");
}

-(void)testGetUnoccupiedPosition{
    [tictactoeEngine start];
    int position = [tictactoeEngine getUnoccupiedPosition];
    STAssertTrue(position == 2, @"Position should be 2, and it is %d",position);
}
-(void)testPlayGameWithAlternatePlayersOccupyingPosition{
    [tictactoeEngine start];
    int oposition = [tictactoeEngine placePegAt:2];
    STAssertTrue([[tictactoeEngine getPegAt:oposition] isEqualToString:@"C"], @" Value at 3 is-%@*",[tictactoeEngine getPegAt:oposition]);
    oposition = [tictactoeEngine placePegAt:6];
    STAssertTrue([[tictactoeEngine getPegAt:oposition] isEqualToString:@"C"], @" Value at 4 is-%@*",[tictactoeEngine getPegAt:oposition]);

}
-(void)testCheckPlacePegAtAlreadyOccupiedPosition{
    [tictactoeEngine start];
    [tictactoeEngine placePegAt:2];
    STAssertThrows([tictactoeEngine placePegAt:2], @"Exception should be thrown");
}

-(void)testCheckIfXIsGettingPlaced{
    [tictactoeEngine start];
    [tictactoeEngine placePegAt:2];
    [tictactoeEngine placePegAt:4];
    
    STAssertTrue([[tictactoeEngine getPegAt:1] isEqualToString:@"C" ], @"Peg at squarePosition 1 is C");
    STAssertTrue([[tictactoeEngine getPegAt:2] isEqualToString:@"X" ], @"Peg at squarePosition 2 is X");
    STAssertTrue([[tictactoeEngine getPegAt:4] isEqualToString:@"X" ], @"Peg at squarePosition 4 is X");
}

-(void)testPlayGameByPlacingXAtAPosition{
    int squarePosition = 2;
    [tictactoeEngine placePegAt:squarePosition];
    STAssertTrue([[tictactoeEngine getPegAt:squarePosition] isEqualToString:@"X" ], @"Peg at squarePosition 2 is X");
}


-(void)testStartGameByPlacingOAtAPosition{
    int squarePosition = 1;
    [tictactoeEngine start];
    STAssertTrue([[tictactoeEngine getPegAt:squarePosition] isEqualToString:@"C" ], @"Peg at squarePosition 1 is C");
}

-(void)testInitialPlayerIsO{
    NSLog(@"%@",tictactoeEngine.currentPlayer);
    STAssertTrue([tictactoeEngine.currentPlayer isEqualToString:@"C"], @"Initial player is C");
}
-(void)testGameOverIsFalseInTheBeginning{
    STAssertFalse(tictactoeEngine.gameOver, @"GameOver should be false");
}

-(void)testTicTacToeEngineInitializedWith9Squares{
    STAssertTrue([tictactoeEngine.squares count] == 9, @"TicTacToeEngine squares are 9");
}
-(void)testTicTacToeEngineNotNull{
    STAssertNotNil(tictactoeEngine, @"TicTacToeEngine is not null");
}
@end

The TicTacToeEngine class that evolved out of TDD approach is shown below.

//TicTacToeEngine.h
@interface TicTacToeEngine : NSObject

@property NSMutableDictionary* squares;
@property bool gameOver;
@property NSString* currentPlayer;
@property NSString* winner;

-(TicTacToeEngine*)init;
-(void)start;
-(NSString*)getPegAt:(int)position;
-(int)placePegAt:(int)position;
-(int)getUnoccupiedPosition;
@end
//TicTacToeEngine.m
@implementation TicTacToeEngine
@synthesize squares;
@synthesize gameOver;
@synthesize currentPlayer;
@synthesize winner;

-(int)getUnoccupiedPosition{
    int position = -1;
    for (int i=1; i<=[squares count]; i++) {
        NSString* key = [[NSString alloc]initWithFormat:@"%d",i];
        NSString* val = [squares objectForKey:key];
        if([val isEqualToString:@""]){
            position = i;
            break;
        }
    }
    return position;
}
-(void)checkRows7And8And9{
    NSString* keyA = [[NSString alloc]initWithFormat:@"%d",7];
    NSString* valA = [squares objectForKey:keyA];
    NSString* keyB = [[NSString alloc]initWithFormat:@"%d",8];
    NSString* valB = [squares objectForKey:keyB];
    NSString* keyC = [[NSString alloc]initWithFormat:@"%d",9];
    NSString* valC = [squares objectForKey:keyC];
    if(!([valA isEqualToString:@""] || [valB isEqualToString:@"" ] || [valC isEqualToString:@""])){
        if([valA isEqualToString:valB] && [valB isEqualToString:valC]){
            gameOver = true;
        }
    }
}

-(void)checkRows4And5And6{
    NSString* keyA = [[NSString alloc]initWithFormat:@"%d",4];
    NSString* valA = [squares objectForKey:keyA];
    NSString* keyB = [[NSString alloc]initWithFormat:@"%d",5];
    NSString* valB = [squares objectForKey:keyB];
    NSString* keyC = [[NSString alloc]initWithFormat:@"%d",6];
    NSString* valC = [squares objectForKey:keyC];
    if(!([valA isEqualToString:@""] || [valB isEqualToString:@"" ] || [valC isEqualToString:@""])){
        if([valA isEqualToString:valB] && [valB isEqualToString:valC]){
            gameOver = true;
        }
    }
}

-(void)checkCellsMatch:(int)cellA :(int)cellB :(int)cellC{
    NSString* keyA = [[NSString alloc]initWithFormat:@"%d",cellA];
    NSString* valA = [squares objectForKey:keyA];
    NSString* keyB = [[NSString alloc]initWithFormat:@"%d",cellB];
    NSString* valB = [squares objectForKey:keyB];
    NSString* keyC = [[NSString alloc]initWithFormat:@"%d",cellC];
    NSString* valC = [squares objectForKey:keyC];
    if(!([valA isEqualToString:@""] || [valB isEqualToString:@"" ] || [valC isEqualToString:@""])){
        if([valA isEqualToString:valB] && [valB isEqualToString:valC]){
            gameOver = true;
        }
    }
}
-(void)checkGameIsOver{
    [self checkCellsMatch:1 :2 :3];
    [self checkCellsMatch:4 :5 :6];
    [self checkCellsMatch:7 :8 :9];

    
    [self checkCellsMatch:1 :4 :7];
    [self checkCellsMatch:2 :5 :8];
    [self checkCellsMatch:3 :6 :9];
    
    [self checkCellsMatch:1 :5 :9];
    [self checkCellsMatch:3 :5 :7];
}

- (int)placeO {
    NSString *key;
    int oposition = [self getUnoccupiedPosition];
    key = [[NSString alloc]initWithFormat:@"%d",oposition];
    [squares setObject:@"C" forKey:key];
    [self checkGameIsOver];
    if(gameOver)
        winner = @"C";
    return oposition;
}

-(int)placePegAt:(int)position{
    int oposition = -1;
    if(gameOver)
        [TicTacToeException raise:@"Game is over" format:@"Game is already over"];
    
    NSString* key = [[NSString alloc]initWithFormat:@"%d",position];
    NSString* val = [squares objectForKey:key];
    if([val isEqualToString:@"X"] || [val isEqualToString:@"C"])
        [TicTacToeException raise:@"Already occupied position" format:@"Cannot place peg at position %d",position];
    [squares setObject:@"X" forKey:key];
    [self checkGameIsOver];
    if(!gameOver){
        oposition = [self placeO];
    }
    else{
        winner = @"X";
    }
    return oposition;
}

-(void)start{
    [squares setObject:@"C" forKey:@"1"];
}
-(NSString*)getPegAt:(int)position{
    NSString* key = [[NSString alloc]initWithFormat:@"%d",position];
    return [squares objectForKey:key];
}
-(TicTacToeEngine*)init{
    self = [super init];
    self.gameOver = false;
    self.currentPlayer = @"C";
    self.squares = [[NSMutableDictionary alloc]initWithObjectsAndKeys:
                    @"",@"1",
                    @"",@"2",
                    @"",@"3",
                    @"",@"4",
                    @"",@"5",
                    @"",@"6",
                    @"",@"7",
                    @"",@"8",
                    @"",@"9"
                    , nil];
    return self;
}
@end

Leave a Reply