Skip to content

Instantly share code, notes, and snippets.

@KaneCheshire
Created October 23, 2019 07:59
Show Gist options
  • Save KaneCheshire/8fc30a6f81116829ea7b6024431665df to your computer and use it in GitHub Desktop.
Save KaneCheshire/8fc30a6f81116829ea7b6024431665df to your computer and use it in GitHub Desktop.
Example on how to mock JSONDecoder for tests
final class MockDecoder: Decoder {
var valuesForKeys: [String: Decodable] = [:]
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey {
return KeyedDecodingContainer(MockContainer(valuesForKeys: valuesForKeys))
}
let codingPath: [CodingKey] = []
let userInfo: [CodingUserInfoKey: Any] = [:]
func unkeyedContainer() throws -> UnkeyedDecodingContainer { fatalError() }
func singleValueContainer() throws -> SingleValueDecodingContainer { fatalError() }
}
extension MockDecoder {
struct MockContainer<Keys: CodingKey>: KeyedDecodingContainerProtocol {
let valuesForKeys: [String: Decodable]
let codingPath: [CodingKey] = []
let allKeys: [Keys] = []
var context: DecodingError.Context { return DecodingError.Context(codingPath: codingPath, debugDescription: "") }
func decode(_ type: String.Type, forKey key: Keys) throws -> String {
guard let value = valuesForKeys[key.stringValue] else { throw DecodingError.keyNotFound(key, context) }
guard let string = value as? String else { throw DecodingError.valueNotFound(type, context) }
return string
}
func decode<T>(_ type: T.Type, forKey key: Keys) throws -> T where T: Decodable {
guard let value = valuesForKeys[key.stringValue] else { throw DecodingError.keyNotFound(key, context) }
guard let decodable = value as? T else { throw DecodingError.valueNotFound(type, context) }
return decodable
}
func contains(_ key: Key) -> Bool { fatalError() }
func decodeNil(forKey key: Keys) throws -> Bool { fatalError() }
func decode(_ type: Bool.Type, forKey key: Keys) throws -> Bool { fatalError() }
func decode(_ type: Double.Type, forKey key: Keys) throws -> Double { fatalError() }
func decode(_ type: Float.Type, forKey key: Keys) throws -> Float { fatalError() }
func decode(_ type: Int.Type, forKey key: Keys) throws -> Int { fatalError() }
func decode(_ type: Int16.Type, forKey key: Keys) throws -> Int16 { fatalError() }
func decode(_ type: Int8.Type, forKey key: Keys) throws -> Int8 { fatalError() }
func decode(_ type: Int32.Type, forKey key: Keys) throws -> Int32 { fatalError() }
func decode(_ type: Int64.Type, forKey key: Keys) throws -> Int64 { fatalError() }
func decode(_ type: UInt.Type, forKey key: Keys) throws -> UInt { fatalError() }
func decode(_ type: UInt8.Type, forKey key: Keys) throws -> UInt8 { fatalError() }
func decode(_ type: UInt16.Type, forKey key: Keys) throws -> UInt16 { fatalError() }
func decode(_ type: UInt32.Type, forKey key: Keys) throws -> UInt32 { fatalError() }
func decode(_ type: UInt64.Type, forKey key: Keys) throws -> UInt64 { fatalError() }
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Keys) throws -> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey { fatalError() }
func nestedUnkeyedContainer(forKey key: Keys) throws -> UnkeyedDecodingContainer { fatalError() }
func superDecoder() throws -> Decoder { fatalError() }
func superDecoder(forKey key: Keys) throws -> Decoder { fatalError() }
}
}
@KaneCheshire
Copy link
Author

KaneCheshire commented Oct 23, 2019

Usage:

let mockDecoder = MockDecoder()
mockDecoder. valuesForKeys = [
  "url": URL(string: "https://example.com")!, // Values can be anything that is `Decodable`
  "description": "Lorem ipsum"
]

Additionally, if you've exposed your CodingKeys enum in your model object, you can make it conform to CaseIterable and then loop through so you get compiler help if you add or remove a case from the enum:

mockDecoder. valuesForKeys = MyModel.CodingKeys.allCases.reduce(into: [:]) { result, key in
  switch key {
  case .url: result[key.stringValue] = URL(string: "https://example.com")!
  case .description: result[key.stringValue] = "Lorem ipsum"
  }
}

Then in your tests you'd just call the init of your Decodable object by passing in our mock decoder, rather than the regular JSONDecoder, removing or changing values for keys as you see fit:

func test_whatHappensIfURLIsMissing() {
  mockDecoder.valuesForKeys[MyModel.CodingKeys.url.stringValue] = nil
  XCTAssertThrowsError(try MyModel(from: mockDecoder) { error in
    // Assert `error` is the error we're expecting
  }
}

You could also then extend this to control what errors get thrown, and support more of the decode functions (which I didn't need for my tests so left them with fatalError for now).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment