iOS Unit Tests - Testing models creation (Part 1)
This is the part 1 in the series of articles on 'Unit Testing on iOS'. Below is the list of all articles and respective links to them.
- iOS Unit Tests - Testing models creation (Part 1)
- iOS Unit Testing - Testing View Controller (Part 2)
- iOS Unit Testing - Switching method implementations with OCMock (Part 3)
- iOS Testing - Testing asynchronous code (Part 4)
- iOS Testing - Input fields validation testing (Part 5)
We have recently started writing iOS unit tests at work. Not that we completely avoided them before, but we were not as consistent with them as we should have been.
Since our most codebase is still in Objective-C, as a practice, I created a sample project for unit tests in the same language. Below are examples of some of the code samples and unit tests I added as a part of exercise. You can clone the unit tests project from Github Repository. I have used following frameworks in order to make this demo and facilitate unit tests. This is how my podfile
looks like
platform :ios, '8.0'
xcodeproj 'JKUnitTestsDemo'
target :JKUnitTestsDemo do
pod 'ReactiveCocoa', '~> 2.0'
pod 'BlocksKit', '~> 2.2'
end
target :JKUnitTestsDemoTests do
pod 'Specta', '~> 1.0'
pod 'OCMock', '~> 3.3'
pod 'Expecta', '~> 1.0'
end
You can find the full code in Github repository
If you have cocoapods
installed on your machine, you can simply run pod install
in the root directory of project.
In this first part I will show you how to test your model objects and mock network request
Say we have a teeny-tiny app for a matrimonial matchmaker. Obviously, as an app it has API setup along with object model to convert list of bride/grooms into objects. The following API will give us the list of brides from server.
For the sake of this demo, I am not actually fetching this data from API, but getting it from local
JSON
file
+ (RACSignal*)brides {
NSString* filePath = [[NSBundle mainBundle] pathForResource:@"brides" ofType:@"json"];
NSData* data = [NSData dataWithContentsOfFile:filePath];
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
NSArray* brides = [json[@"brides"] bk_map:^id(NSDictionary* br) {
return [[JKBride alloc] initWithDictionary:br];
}];
return [[RACSignal createSignal:^RACDisposable *(id subscriber) {
[subscriber sendNext:brides];
return nil;
}] delay:1.0];
}
And this is how my Javascript object looks like,
{
"success": true,
"brides": [
{
"first_name": "Berta",
"last_name": "Johnson",
"city": "Boston",
"cast": "Wanjari",
"married": true,
"education": "masters",
"income": 60000
},
{
"first_name": "Leena",
"last_name": "Crident",
"city": "Pune",
"cast": "CKP",
"married": false,
"education": "bachelors",
"income": 30000
},
{
"first_name": "Jacqueline",
"last_name": "Sharp",
"city": "Virar",
"cast": "Pachkalshi",
"married": true,
"education": "bachelors",
"income": 10000
},
{
"first_name": "Reggie",
"last_name": "Love",
"city": "Vasai",
"cast": "Pachkalshi",
"married": true,
"education": "masters",
"income": 5000
},
{
"first_name": "Jessica",
"last_name": "Roberts",
"city": "Vasai",
"cast": "Pachkalshi",
"married": false,
"education": "bachelors",
"income": 40000
}
]
}
And this is the model for converting these dictionaries in the JKBride
object model
Header file
@interface JKBride : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, copy) NSString* city;
@property (nonatomic, copy) NSString* cast;
@property (nonatomic, assign) BOOL married;
@property (nonatomic, copy) NSString* education;
@property (nonatomic, assign) NSInteger income;
- (instancetype)initWithDictionary:(NSDictionary*)brideInfo;
@end
Main(implementation) file
@implementation JKBride
- (instancetype)initWithDictionary:(NSDictionary*)brideInfo {
self = [super init];
if (!self) { return nil; }
_name = [NSString stringWithFormat:@"%@ %@", brideInfo[@"first_name"], brideInfo[@"last_name"]];
_city = brideInfo[@"city"];
_cast = brideInfo[@"cast"];
_married = [brideInfo[@"married"] boolValue];
_education = brideInfo[@"education"];
_income = [brideInfo[@"income"] integerValue];
return self;
}
@end
And finally below is the unit tests that will be used to test the fetching and model object conversion. As mentioned earlier, we are going to use Specta and OCMock to perform testing.
// Declare all required variables and setup the initial value for models as required for testing.
SpecBegin(ViewModelTests)
__block NSArray* brides;
beforeAll(^{
// This is the sample brides dictionary we are assuming will come from API, but in fact it comes from local resource.
NSDictionary* bridesDict = @{
@"success": @1,
@"brides": @[
@{
@"first_name": @"Berta",
@"last_name": @"Johnson",
@"city": @"Boston",
@"cast": @"Wanjari",
@"married": @1,
@"education": @"masters",
@"income": @60000
},
@{
@"first_name": @"Leena",
@"last_name": @"Crident",
@"city": @"Pune",
@"cast": @"CKP",
@"married": @0,
@"education": @"bachelors",
@"income": @30000
},
@{
@"first_name": @"Jacqueline",
@"last_name": @"Sharp",
@"city": @"Virar",
@"cast": @"Pachkalshi",
@"married": @0,
@"education": @"bachelors",
@"income": @10000
}
]
};
// Convert the collection of bride dictionaries into JKBride objects. We will test it later.
brides = [bridesDict[@"brides"] bk_map:^id(id obj) {
return [[JKBride alloc] initWithDictionary:obj];
}];
// Mock the call to static method [JKBridesProvider brides] so that it returns our stubbed data instead of expecting it from the server.
id classMock = OCMClassMock([JKBridesProvider class]);
OCMStub(ClassMethod([classMock brides])).andReturn([RACSignal return:brides]);
});
// Above code sets up the environment for testing. in the following code we will actually test the models by comparing actual and expected values.
describe(@"Verifying the model creation", ^{
it(@"Model Creation", ^{
JKBride* firstBride = [brides firstObject];
JKBride* lastBride = [brides lastObject];
XCTAssert([firstBride.name isEqualToString:@"Berta Johnson"]);
XCTAssert([firstBride.city isEqualToString:@"Boston"]);
XCTAssert([firstBride.cast isEqualToString:@"Wanjari"]);
XCTAssertEqual(firstBride.married, true);
XCTAssert([firstBride.education isEqualToString:@"masters"]);
XCTAssert([lastBride.name isEqualToString:@"Jacqueline Sharp"]);
XCTAssert([lastBride.city isEqualToString:@"Virar"]);
XCTAssert([lastBride.cast isEqualToString:@"Pachkalshi"]);
XCTAssertEqual(lastBride.married, false);
XCTAssert([lastBride.education isEqualToString:@"bachelors"]);
});
});
And that should be it to mock your API under consideration and object model. If you want you can perform rigorous testing by passing nil
, NULL
or empty objects to object initializer. As per test, those objects will have nil, empty, blank or invalid properties assigned to them. But this can be good starting point to write tests.
In the next post I will write about how testing view controllers - Especially testing associated with
UITableView
subclass datasource and delegates