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:

Initializing a struct with a custom codable initializer has no stored properties to define

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:

Memberwise initializer is preserved when moving custom initializer to an extension

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:

When asserting two values are equal, add Equatable conformance

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:

XCTAssert fails and shows a lengthy error

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:

XCTAssert error when using a library called Difference

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!