Testing Model Layer of Swift Apps Using Mockingjay
The model layer is the very foundation of your app and should be rock solid. I’ve seen way too many codebases during my career where an invalid JSON and a 500 error page returned by an API would cause the completion block not being called or an empty model object being returned. Let’s make sure it won’t happen by writing some unit tests.
Your main concern may be whether it will take writing a mock version of Alamofire or whatever netwroking stack you’re using. The answer is no and the solution is going to be super simple as long as you’re using NSURLSession, which you probably are.
The Model Layer
Let’s declare a simple model object as a swift struct because swift structs are awesome for this purpose.
struct UserEntity {
var name: String
var avatarURL: NSURL
var id: String
var aboutMe: String?
var age: UInt
var distanceKM: Double
var songs = [SongEntity]()
var gender: Gender
var instagramUsername: String?
}We’ll extend UserEntity struct with an initializer that takes JSON object and throws an exception in case provided JSON is invalid.
import SwiftyJSONextension UserEntity {
init(json: JSON) throws {
if let id = json["id"].string where id.characters.count > 0 {
self.id = id
} else {
throw EntityError.InvalidInput
}
if let avatarURLString = json["avatarURL"].string, let avatarURL = NSURL(string: avatarURLString) {
self.avatarURL = avatarURL
} else {
throw EntityError.InvalidInput
}
if let aboutMe = json["aboutMe"].string where name.characters.count > 0 {
self.aboutMe = aboutMe
}
if let gender = json["gender"].string where gender.characters.count > 0 {
self.gender = Gender(string: gender)
} else {
throw EntityError.InvalidInput
}
if let age = json["age"].uInt {
self.age = age
} else {
throw EntityError.InvalidInput
}
if let songsJSONArray = json["songs"].array {
for songJSON in songsJSONArray {
try songs.append(SongEntity(json: songJSON))
}
}
// Some validations were omitted to shorten this snippet.
}
We’ll extend UserEntity further to be able to fetch a user from a REST API endpoint using Alamofire.
import Alamofire
extension UserEntity {
static func requestUser(userId: String, accessToken: String, completion: (result: AsyncResult<UserEntity, NSError>) -> Void) {
request(.GET, "\(Configuration().APIAddress)/api/user/\(userId)?accessToken=\(accessToken)", parameters: nil).validate().responseJSON { (response) in
if let json = response.result.value as? [String : AnyObject] {
if let userDictionary = json["user"] as? [String : AnyObject] {
let userJSON = JSON(userDictionary)
do {
let user = try UserEntity(json: userJSON)
completion(result: AsyncResult.Success(user))
} catch {
completion(result: AsyncResult.Failure(NSError.msc_unexpectedDataError()).reportFailure())
}
} else {
completion(result: AsyncResult.Failure(NSError.msc_unexpectedDataError()).reportFailure())
}
} else {
completion(result: AsyncResult.Failure(response.result.error!).reportFailure())
}
}
}
}The Unit Tests
Let’s write some unit tests to test following scenarios
- The API returns a valid user JSON
- The API returns an invalid user JSON
- The API returns unexpected data, like a 500 error page
We are going stub some API paths using Mockingjay, meaning that they will return our custom tailored data instead the actual response that would be returned by the REST API.
import XCTest
import Mockingjayclass MusicDatingTests: XCTestCase {
override func setUp() {
super.setUp()
let serverErrorStub = http(500, headers: nil, data: "uh oh".dataUsingEncoding(NSUTF8StringEncoding))
let notFoundStub = http(404, headers: nil, data: nil)
let userData = NSData(contentsOfFile: NSBundle(forClass: self.dynamicType).pathForResource("user", ofType: "json")!)!
let invalidUserData = NSData(contentsOfFile: NSBundle(forClass: self.dynamicType).pathForResource("invalid_user", ofType: "json")!)!
stub(uri("/api/user/5744b138f64cf2021002fa76"), builder: jsonData(userData))
stub(uri("/api/user/5744b138f64cf2021002fa77"), builder: jsonData(invalidUserData))
stub(uri("/api/user/thisWillNeverBeAUserId"), builder: notFoundStub)
}
}
The setup is as following.
- Calling /api/user/5744b138f64cf2021002fa76 will return a valid user JSON
- Calling /api/user/5744b138f64cf2021002fa77 will return an invalid user JSON, userId will be an empty string.
- Calling /api/user/thisWillNeverBeAUserId will return a response with 404 status code.
It’s important to note that we haven’t made any changes to our model or networking code in the app, Mockingjay uses method swizzling to replace NSURLSessionConfiguration.defaultSessionConfiguration() with a custom implementation, you can check out the source code to see what’s going on exactly.
Get Petr Pavlík’s stories in your inbox
Join Medium for free to get updates from this writer.
Writing the tests themselves is quite straightforward, we expect to receive either a UserEntity instance, or an error, based on the endpoint we’re calling.
func testFetchExistingUserUser() {
let fetchExistingUserExpectation = expectationWithDescription("fetch existing user")
UserEntity.requestUser("5744b138f64cf2021002fa76", accessToken: "fakeAccessToken") { (result) in
XCTAssertNotNil(result.value)
fetchExistingUserExpectation.fulfill()
}
waitForExpectationsWithTimeout(5, handler: nil)
}
func testNonexistentUser() {
let fetchUserExpectation = expectationWithDescription("fetch nonexistent user")
UserEntity.requestUser("thisWillNeverBeAUserId", accessToken: "fakeAccessToken") { (result) in
XCTAssertNotNil(result.error)
fetchUserExpectation.fulfill()
}
waitForExpectationsWithTimeout(5, handler: nil)
}
func testInvalidUser() {
let fetchUserExpectation = expectationWithDescription("fetch invalid user data")
UserEntity.requestUser("5744b138f64cf2021002fa77", accessToken: "fakeAccessToken") { (result) in
XCTAssertNotNil(result.error)
fetchUserExpectation.fulfill()
}
waitForExpectationsWithTimeout(5, handler: nil)
}Running Unit Tests
I’m currently using a CI service called buddybuild to to make sure the app compiles and run tests each time someone pushes to the github repo, you may want to give it a try.







