Sunday, January 8, 2012

Unit Testing

This report shows an example of unit testing in Objective-C using iPhone Unit Testing framework for a map view based application. Unit tests target your code at the lowest possible level - the input/output of a single method (a method is often the smallest testable unit of code). If every single method behaves as you expect, so will your program. What percentage of your code is covered by unit tests is called unit test coverage. 100% coverage is not only rare, but unrealistic.  You should determine which classes and methods are to get the most attention from your tests. This can be achieved by answering a few questions like how often the implementation changes or if it takes input from an unpredictable source, like a user or a network connection.

External data sources are less reliable than, for example, a static read-only file on the file system. When applications rely on external data sources such as the Internet and device hardware, availability and validity of data are undoubtedly major risks. This is due to the fact that the likelihood of incidents such as information has been moved, information not being in expected format or network disconnection is high. Therefore, checking if an application handles data related matters properly can be the centre of focus in tests.

Example map view application
The example map view application in question here consists of two view controllers. First, there is a table view controller which loads some coordinates from a CSV file. By selecting any of the coordinates from the table view, the second view, the map view, appears showing the chosen point. Screen shots of the application are presented below (source code of the application is provided at the end of the report).

Figure 1: example map view application

The goal here is to make sure a proper error is raised when coordinates, either latitude or longitude, are out of range. The code snippet below shows data validation in the map view controller. Note that all data handling and validation methods are supposed to be in a separate class in model layer. However, as the goal here is not to demonstrate MVC pattern and to make it quicker, data related matters are being handled in view controllers.

- (void)setMapDetailsWithLatitude:(double) latitude andWithLongitude:(double)longitude
{
    @try {
       
        if ( latitude > 90 || latitude < -90 ) {
            NSException* e = [NSException exceptionWithName:@"Bad Location" 
                                          reason:@"Invalid latitude" 
                                          userInfo:nil];
            @throw e;
        }

        if ( longitude > 180 || longitude < -180 ) {
            NSException* e = [NSException exceptionWithName:@"Bad Location" 
                                          reason:@"Invalid longtitude" 
                                          userInfo:nil];
            @throw e;
        }
       
        MKCoordinateSpan coordinationSpan;
        coordinationSpan.latitudeDelta = 0.1;
        coordinationSpan.longitudeDelta = 0.1;
       
        CLLocationCoordinate2D locationCoordinate;
        locationCoordinate.latitude = latitude;
        locationCoordinate.longitude = longitude;
       
        MKCoordinateRegion region;
        region.span=coordinationSpan;
        region.center=locationCoordinate;
       
        mapView.mapType = MKMapTypeStandard;
        [mapView setShowsUserLocation:YES];
        [mapView setRegion:region animated:TRUE];     
    }
    @catch (NSException *exception) {
        NSException* e = [NSException exceptionWithName:@"Bad Location" 
                                      reason:@"Invalid Location Parameters" 
                                      userInfo:[exception userInfo]];
        @throw e;
    }
}
Code snippet 1: data validation

Fortunately, XCODE makes it fairly easy to add unit tests to applications. What needs to be done in the beginning is to make sure Include Unit Tests checkbox is checked when creating a new application.

Figure 2: including unit tests when creating a new project

Then all needs to be done is clicking and holding Run button in the upper left of XCODE IDE, and choose test from the popup menu.

Figure 3: build for testing

If you forgot to include unit tests in an application, or you want to add unit tests to an existing application, you simply add a new target to the project, and add a “Cocoa Touch Unit Testing Bundle” to your app.

Figure 4: adding a new target to project

Figure 5: add a Cocoa Touch Unit Testing Bundle to project

Next is to write appropriate test cases to see if data validation code works properly and effectively. There are 4 test cases in the example map view application.
  • testInvalidLatitude – Tests if an invalid latitude would cause an exception or not (latitude should be between -90 and +90).
  • testInvalidLongitude - Tests if an invalid longitude would cause an exception or not (longitude should be between -180 and +180).
  • testValidCoords – Tests if valid latitude and longitude would not cause an exception.
  • testAllCoordsFromFile – Tests all the values provided in the gps_coords.csv file.
Note that test cases names should follow the above format, which is “test” plus some other words as in testInvalidLatitude, to be considered as test cases and get executed. Below, the code for test cases mentioned above is presented.

- (void)testInvalidLatitude
{
    MapViewController* mapVC = [[MapViewController alloc] initWithNibName:@"MapViewController" bundle:nil];
   
    STAssertThrowsSpecificNamed([mapVC setMapDetailsWithLatitude:91 andWithLongitude:0],NSException, @"Bad Location", @"Invalid latitude or longitude values should raise an exception");
   
    [mapVC release];
}

- (void)testInvalidLongitude
{
    MapViewController* mapVC = [[MapViewController alloc] initWithNibName:@"MapViewController" bundle:nil];
  
    STAssertThrowsSpecificNamed([mapVC setMapDetailsWithLatitude:0 andWithLongitude:181],NSException, @"Bad Location", @"Invalid latitude or longitude values should raise an exception");
   
    [mapVC release];
}

- (void)testValidCoords
{
    MapViewController* mapVC = [[MapViewController alloc] initWithNibName:@"MapViewController" bundle:nil];
   
    STAssertNoThrow([mapVC setMapDetailsWithLatitude:27 andWithLongitude:153],@"Valid latitude or longitude values should NOT raise an exception");
   
    [mapVC release];
}

- (void)testAllCoordsFromFile
{
    NSString* filePath = [[NSBundle mainBundle] pathForResource:@"gps_coords"
                                                ofType:@"csv"];
    NSString* gpsCoords = [NSString stringWithContentsOfFile:filePath
                                    usedEncoding:nil
                                    error:nil];
    NSMutableArray* mutableArray = [[NSMutableArray alloc] initWithArray:[gpsCoords componentsSeparatedByString:@"\n"]];
   
    MapViewController* mapVC = [[MapViewController alloc] initWithNibName:@"MapViewController" bundle:nil];
   
    double lat;  
    double lng;
   
    for (NSInteger i=0; i<[mutableArray count]; i++) {
        @try {
            lat = [[[[mutableArray objectAtIndex:i] componentsSeparatedByString:@";"] objectAtIndex:0] doubleValue];       
            lng = [[[[mutableArray objectAtIndex:i] componentsSeparatedByString:@";"] objectAtIndex:1] doubleValue];

            STAssertNoThrow([mapVC setMapDetailsWithLatitude:lat andWithLongitude:lng],@"Valid latitude or longitude values should NOT raise an exception");
        }
        @catch (NSException* e){
            NSLog([NSString stringWithFormat:@"line %d is not in expected format",i]);
        }      
    }   
   
    [mapVC release];
    [mutableArray release];
}
Code snippet 2: test cases in the test class
The results of tests are shown in XCODE output window (as shown in figure 6 bolew). Tests marked with passed keyword are successful tests. It means that, for instance, expected exception was thrown, or no exception was thrown when it was not meant to, or the return value of some method are as expected and so on.

Figure 6: test results

Conclusion
Variety of tests can be implemented such as STAssertNotNil, STAssertTrue, STAssertThrows, STAssertThrowsSpecific, STAssertThrowsSpecificNamed and STAssertEquals. In addition, tests are debug-able using XCODE which is very helpful. Taking advantage of unit testing, to some extent, can guarantee the functionality of any application. Having a variety of test cases that are designed and implemented accurately can help to minimize the risk of failure. However, unit testing is absolutely not a substitute for pressure tests or tests including real users whatsoever. Unit testing is specifically useful to test the desired functionality of the application in known dangerous circumstances, where you know your code might break.

No comments:

Post a Comment