How to test custom Codable initializer
Sometimes, when parsing a JSON, you need to implement a custom Codable
initializer. Whether Decodable
or Encodable
, depends on the use-case.
Because of that custom logic, you're no longer using the default implementation. It's a good idea to test your code.
Custom Decodable initializer
Consider the following struct with a custom decodable initializer:
struct Conference: Codable {
let name: String
let city: String
let country: String
let date: String
let days: Int
let isOnline: Bool
enum CodingKeys: String, CodingKey {
case name, city, country, date, days, isOnline
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
city = try container.decode(String.self, forKey: .city)
country = try container.decode(String.self, forKey: .country)
date = try container.decode(String.self, forKey: .date)
days = try container.decode(Int.self, forKey: .days)
isOnline = (try? container.decode(Bool.self, forKey: .isOnline)) ?? false
}
}
Instead of creating an optional with a default value, you create a custom decoder for isOnline
in the initializer. If the key is missing, give it a default value of false
.
Mock the data
First, you need to create a mock data for the Conference
. If you try to create a conference, you'll see you can't initialize a Conference
object using its properties:
When you create custom decodable initializer, you lose the memberwise initializer.
Memberwise initializer
Structure types automatically get a default initializer, called memberwise initializer. It's generated by compiler based on structure's stored properties or members. But, if you define custom initializer, you lose the default memberwise initializer.
You can learn more about memberwise initializers in the Apple's Swift Language Guide.
Luckily, there's an easy solution. To preserve the memberwise initializer, you need to move the initializer to an extension:
extension Conference {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
city = try container.decode(String.self, forKey: .city)
country = try container.decode(String.self, forKey: .country)
date = try container.decode(String.self, forKey: .date)
days = try container.decode(Int.self, forKey: .days)
isOnline = (try? container.decode(Bool.self, forKey: .isOnline)) ?? false
}
}
Now you have both your custom initializer and the memberwise initializer:
With this, you can create some mock data:
struct Seed {
static let conference = Conference(
name: "iOSDevUK",
city: "Aberystwyth",
country: "United Kingdom",
date: "04/09/2022",
days: 4,
isOnline: false
)
}
Mock the JSON
To test if your custom decodable implementation works, you also need to mock the JSON data:
private let conferenceData = Data("""
{
"name": "iOSDevUK",
"city": "Aberystwyth",
"country": "United Kingdom",
"date": "04/09/2022",
"days": 4
}
""".utf8)
Unit test
Pass the JSON data to the JSONDecoder.decode(_:from:)
and test whether the decoded output is equal to the expected conference
seed:
func test_customDecoding_returnsExpectedValue() {
//Given
let conference = Seed.conference
//When
let decodedConference = try? JSONDecoder().decode(Conference.self, from: conferenceData)
//Then
XCTAssertEqual(
decodedConference,
conference,
"Decoded JSON doesn't match the given Conference() object"
)
}
Now, Xcode will complain once more:
Because you're testing for equality, you need to add the Equatable
conformance to the Conference
struct:
struct Conference: Codable, Equatable {
...
}
That's it! You should have a passing unit test for your custom decodable initializer.
Make sure your conference seed and the JSON you're testing with have identical data. If one value is incorrect, the test will fail.
Failing test
If your test fails, XCTAssertEqual
doesn't really give any useful information on why and where the test failed:
Imagine testing against an array of data or a large nested JSON. It would be impossible to look where the test failed.
Krzysztof Zabłocki (@merowing_), created a pretty cool library to help you identify the difference between two instances, conveniently called Difference.
When you import the library to your project, instead of the obscure error message, you'll see this:
Pretty cool, right? I use the library in my projects and I can tell you it helps a lot when you have large JSON files. Thank you Krzysztof!
Please feel free to reach out on Twitter if you have any questions, comments, or feedback.
Thank you for reading and happy coding!