
JSON (JavaScript Object Notation) is a lightweight data-interchange format which is built on name/value pairs or ordered list of values. In recent times, Almost all apps use JSON in one way or another including the configurations, over the network etc. Since we often use JSON to such a heavy extent, it becomes more intense to deal with the errors, especially in the core data models. It’s therefore important that we have solid tests in network place to make sure its error free.
Unit Testing:
In iOS, usually, test cases are written to just verify the single unit or module of our app. Though these tests are valuable to iterate on separate parts of our app, and it eliminates rework to write the entire test suite, we face a lot of challenges when it comes to JSON mapping. Consider the following user model in our code:
struct User { let name: String let age: Int } Which requires the below JSON structure: { "name": “Mark”, "age": 30 }
To test that our User model could be initialised with above JSON, we first bundle the JSON in our test target, then load it in a test case, and finally verify that a User instance was successfully created.
class UserTests: XCTestCase { func testJSONMapping() throws { let bundle = Bundle(for: type(of: self)) guard let url = bundle.url(forResource: "User", withExtension: "json") else { XCTFail("Missing file: User.json") return } let json = try Data(contentsOf: url) let user: User = try unbox(data: json) XCTAssertEqual(user.name, “Mark”) XCTAssertEqual(user.age, 30) } }
These tests serve to an extent but the problem arises when the JSON that backend sends changes in any way. That ideally should not happen and we must ensure to use proper versioning and integration tests so that a new version of the backend never breaks any users.
The problem in above code occurs since we have bundled a static JSON file in our tests, which will indeed break then once we start requesting real data from the backend.
End-to-end Tests:
In this situations, we bring in end-to-end tests which are really valuable. Its perspective is to extend code coverage with the tests that cover our entire stack vertically. In the JSON tests, we need to test the data that is received from the backend system, which ought to get mapped into a model. Typically, we use UI Testing to perform end-to-end testing, but those are very expensive to run. We don’t necessarily require to run through all the UI components in order to test JSON mapping.
Like in most programming situations, being able to maintain a single source makes things simpler and less error prone. So we could base our client-side JSON mapping tests on real data from the backend and have a single, auto-updating) source.
For auto-downloading the script in real time we have several services. Here goes one such example with marathon(https://github.com/JohnSundell/Marathon) which is a tool that enables you to easily write and run scripts using Swift.
//Download JSON let query = "language:swift" let url = URL(string: “https://git.com”) //Required url let json = try Data(contentsOf: url) // Write JSON file let resourceFolder = try Folder.current.subfolder(atPath: "Tests/Resources") try resourceFolder.createFile(named: "User.json", contents: json) // Save a new version of .lastRun let lastRunFile = try scriptsFolder.createFile(named: ".lastRun") try lastRunFile.write(string: String(Int(currentTimestamp)))
To get the script run, every time the tests are initiated, add a new Run Script phase to the test target in the Xcode’s project navigator, preferably right after Target Dependencies. Call it as ”Download JSON” and add the below content:
marathon run Scripts/DownloadJSON
Now on running the tests a TestResources folder with a Git.json file will be created the root folder of the project. Add the file into Xcode -> test target and we have our auto-updating JSON file.
The following could help writing a test in Xcode,
struct GitFile { let id: Int let value: String } extension GitFile: Unboxable { init(unboxer: Unboxer) throws { id = try unboxer.unbox(key: "id") value = try unboxer.unbox(key: “value") } }
Finally, execute the test that verifies that the downloaded JSON file is indeed compatible with the model,
class RepositoryTests: XCTestCase { func testJSONMapping() throws { let bundle = Bundle(for: type(of: self)) guard let url = bundle.url(forResource: "Git", withExtension: "json") else { XCTFail("Missing file: Git.json”) return } let json = try Data(contentsOf: url) let repositories: [Repository] = try unbox(data: json, atKeyPath: "items") XCTAssertFalse(repositories.isEmpty) } }
And now we have an end-to-end test, that runs fast, auto-updates and makes sure that our JSON mapping code will always work against real data from the backend.
–
Poorvitha Y,
iOS Development Team,
Mallow Technologies.