Home > Blockchain >  Swift Tests: Spying on `Encoder`?
Swift Tests: Spying on `Encoder`?

Time:02-05

I have some Swift models that encode data to json. For example:

struct ExampleModel: Encodable {
    var myComputedProperty: Bool { dependentModel.first(where: { $0.hasTrueProperty}) }

    enum CodingKeys: String, CodingKey {
        case firstKey = "first_key"
        case secondKey = "second_key"
        case myComputedProperty = "my_computed_property"
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(firstKey, forKey: .firstKey)
        try container.encode(myComputedProperty, forKey: .myComputedProperty)
    }
}

The encoded data is sent to an API, and cross-platform system tests in this case are logistically tricky, so it's important I write tests that ensure the encoded data is as expected. All I'm looking to do is ensure container.encode receives expected keys and values.

I'm somewhat new to Swift, and trying to override the container, its dependencies & generics, and its .encode method is taking me down a rabbit-hole of rewriting half Swift's encoding foundation. In short: the spy I'm writing is too complex to be useful.

Despite lack of Google/StackOverflow results, I'm guessing spying on encoders is common (?), and that there's an easier way to confirm container.encode receives expected values. But the way swift's Encoder functionality is written is making it hard for me to do so without rewriting half the encoder. Anyone have boilerplate code or an example of effectively spying on container.encode?

CodePudding user response:

I have never done tests spying on encoder, although I have plenty of tests checking correction of encoding/decoding.

There are multiple ways to do it:

  • You can get something like SwiftyJSON and inspect encoded JSON
  • You can use OHHTTPStubs to mock sending a request to API, and examine the request the way it's sent (this allows to examine not only JSON, but headers as well)

But the most basic way to test is encoding and then decoding your data structure, and comparing the structures. They should be identical.

How it's done:

  1. Create a Decodable extension for your struct (or if your struct is Codable, then you already ave this). It can be added directly in the test class / test target if you don't need it in the production code:
extension ExampleModel: Decodable {
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        firstKey = try container.decode(<...>.self, forKey: .firstKey)
        // etc
    }
}
  1. Add Equatable extension for the struct:
extension ExampleModel: Equatable {
    // Rely on synthesized comparison, or create your own
}
  1. Add a test:
let given = ExampleModel(firstKey: ..., ...)
let whenEncoded = try JSONEncoder().encode(given)
let whenDecoded = try JSONDecoder().decode(ExampleModel.self, from: whenEncoded)
// Then
XCTAssertEqual(given, whenDecoded)

Couple of notes:

  • It's very unusual to encode computed property. It's not prohibited, but it breaks the immutability of the property you are about to send to API: what if it changes after you called encode, but before it was sent to API? Better solution is to make a let property in the struct, but have an option to create a struct with this value given with appropriate calculation (e.g. init for ExampleModel could be passing the dependentModel, and the property would be calculated in the init once)
  • If you still choose to have a calculated property, you obviously cannot decode into it. So in that case you will need to preserve the decoded property in some class variable to compare it separately.
  • Related