Reflect what you fetch
"I switched to mobile because there's less testing"
- Anonymous, South Florida CocoaHeads

We're going to test against three bugs unique (hopefully) to early-stage startups.

Some common scenarios:
  • No "company way" of communicating API changes is set. Some server people barely know the people on their immediate team, let alone the client-side folks, It'd be lucky if any communication at all is happening. Time is money so the API is being built out before hiring is finished, and before people have stopped getting lost looking for the restroom.
  • Things we can't control. JSON payloads from third parties can change in ways small enough to breach any runtime error handling. And baby startups reach for third party libs the way new cooks reach for the salt.
Our hypothetical startup uses MapKit heavily. Some of our locations have names and addresses and others are just CG points. So we want some location names to be nil but is it enough to create a new struct or even a new protocol?
Getting more granular, failing to display the mailing address in the beta 0 could be tolerable but we can't ship whitespace (or worse NULL) where the names of famous landmarks should be, or we'll look less like we're moving fast and breaking things, and more like we don't know how to move while we break the things we can reach.
From the day we hit New > Project in Xcode, our networking unit tests need to be better than average.
So here's what we're going to do...
We have our JSON payload that's hardly in its final form:

{
    "locationId": 1,
    "name": "CVS",
    "latitude": 25.771171,
    "longitude": -80.191777,
    "address": {...}
}

Our basic Location that serializes itself.

struct Location{
    var locationId:Int
    var name:String?
    var latitude:Float?
    var longitude:Float?
    var address:Address?
    
    init(locationId:Int) {
        self.locationId = locationId
    }
    
    static func serialize(with dict:Dictionary<AnyHashable, Any>) -> Location? {
        guard let locationId = dict["locationId"] as? Int else{
            return nil
        }
        var location = Location(locationId:locationId)
        location.name = dict["name"] as? String
        location.latitude = dict["latitude"] as? Float
        location.longitude = dict["longitude"] as? Float

        if let addressDict = dict["address"] as? Dictionary {
            location.address = Address(with dict:addressDict)
        }
        
        return location
    }

Note: the following only works with data from the production (or at least staging) server. We're anticipating the mood swings of a server team that's a teenager in server team years.
In XCTest we're going to use an Assert Equal, optional binding, and the standard lib's Mirror (reflection) API to test an async operation for four potential runtime errors, three of them deadly serious:
  1. Does our JSON payload have more keys than Location has properties or vice versa?
  2. Are the keys being downloaded the keys that we expect?
  3. For this fetch request, for this instance and/or for this scenario, do the necessary values exist or are they nil?
  4. Are the values of the type that we expect?

func testLocationSerialization() {
        let url = URL(string: "<YOUR PRODUCTION OR AT LEAST STAGING SERVER>")
        let promise = expectation(description: "everything cool")
        
         
        //keys that really need to be the correct type and not nil 
        let name = "name"
        
        let dataTask = session.dataTask(with: url!) { data, response, error in
            
            
            ...

                    let JSONResponse:AnyObject
                    do {
                        try JSONResponse = JSONSerialization.jsonObject(with: data, options: []) as AnyObject
                        guard let jsonDict = JSONResponse as? Dictionary<AnyHashable, AnyObject> else{
                            XCTFail("JSON is nil")
                            return
                        }
                        guard let locationDictionaries:Array = (jsonDict["locations_array"]) as? [Dictionary<AnyHashable,Any>] else{
                            XCTFail("JSON has no array of dictionaries")
                            return
                        }
                        for dict in locationDictionaries{
                            if let location:Location = Location.serialize(with: dict){
                                let mirror      = Mirror(reflecting: location)
                                var array:Array = [Any]()
                                var dictionary:Dictionary = [AnyHashable:Any]()
                                
                                //1. Are there keys being fetched that you're not even trying to serialize?
                                XCTAssertEqual(dict.keys.count, Int(mirror.children.count))
                                
                                for case let (key?, value) in mirror.children{
                                    dictionary[key] = value
                                    array.append(key)
                                }
                                
                                //2. Make sure the keys are not nil e.g. guards against someone changing "name" to "title".
                                guard let nameKey = array.first(where: {$0 as? String == name}) else{
                                    XCTFail("Response does not contain key: \(name)")
                                    return
                                }
                                
                                //the key is a string sanity check
                                guard let nameKeyString = nameKey as? String else{
                                    XCTFail("key: \(nameKey) is not a string lol")
                                    return
                                }
                                
                                //3. Make sure specific properties are are not nil e.g. a name must exist for certain Locations that aren't just pin drops.
                                guard let value = dictionary[nameKeyString] else{
                                    XCTFail("Value is nil for the key: \(nameKey)")
                                    return
                                }
                                
                                //4. Make sure values are the right type e.g. the value for "name" changed from a String to a nested object or something.
                                if !(Mirror(reflecting:value).subjectType == Optional<String>.self){
                                    XCTFail("Value for key \(nameKey) is not the right type")
                                }
                                
                            } else {
                                XCTFail("Location failed to serialize")
                            }
                        }
                    } catch {
                        XCTFail("JSONSerialization failed")
                    }

            
            ...


    }

Now we could guard against 2 and 3 by making all these properties non-optional and setting them in the initializer. But alas, there are times when we want the data to sometimes be nil...
For that grey area, use reflection for properties that are not fully protected by Swift's wonderful compile-time checker.
Reflect
We talk about how smooth and automated our build and deploy process will be in meetings, interviews, meetups, mission statements, postmortems, and happy hours. We boast that the client will drive any change to JSON generation. That modern AWS deployment allows us to focus exclusively on delighting our users with animations. The next morning, back at our desks, we get an email or Slack message announcing a deadline bump, or last minute spec change straight from the business team, and we need those endpoints now...
The heady first days of great new companies have changed about as much as programming concepts have changed - the semantics may be different, tools and languages are better designed, but the obstacles to a successful release have kept pace.
Mirror is an underused, almost philosophical API and there are many clever things to discover. The second half of this post, for example, has some interesting Core Data applications.
So stop thinking that unit testing has no effect on your first release and consider that without good coverage, there may not be a second.
Copyright © 2011–2018 Mike Leveton