Asynchronous code is hard and it’s very easy to make subtle mistakes. I look at a real life influenced example where we
accidentally made our unit tests synchronous, therefore reducing the tests effectiveness at catching bugs.
Background
It’s worth repeating that asynchronous code is hard and it’s very easy to make subtle mistakes.
The real life issue manifested in considerably more interesting ways than what follows but the example here is just to highlight the root cause of the problem. One more disclaimer - the code that caused the issue is pretty poorly designed and is a hang over from an old Objective-C code base, which means the following code is in no way idiomatic and one could argue that more modern coding styles would never have allowed this bug.
Apologies out of the way…
We have two functions:
populate(random:) - is a function that will take a Random (a reference type class) and destructively set its number value.
process() - is a synchronous function that creates a Random, feeds it to populate(random:) and then returns the result.
(Yes I know this is a terrible example but please stick with me)
The current tests for this set up look something like this:
Everything is working great and we continue working through our backlog. A year later a new requirement comes in that we should use a fancy new random number generating web service. We integrate this new service and add the infrastructure to make it possible to stub it out in our tests. The end result is not too dissimilar. First we start by updating the RandomNumberGenerator to utilise our new service (in this case I’m just going to call the completion after a short wait).
The above provides a default implementation of remoteNext but allows us to override it in our tests because it’s just a var. The next thing to do is update our tests to provide a stub implementation of remoteNext that we control for testing.
NB: Unit tests are not acceptance tests and in real life we should run the automated/manual acceptance tests before moving on.
Where did it all go wrong?
Some people will have been internally screaming whilst reading the above changes. We’ve made the error of not taking a step back from our implementation and sense checking it. The process function is synchronous but the RandomNumberGenerator.populate(random:) function was updated to be asynchronous - this is not going to work. The issue has been completely masked by the fact that our unit tests were accidentally making the asynchronous RandomNumberGenerator.populate(random:) synchronous.
What does it mean to make “the asynchronous RandomNumberGenerator.populate(random:) synchronous”? Let’s demonstrate by changing the tests to be truly async again:
In lines 6-8 I’m executing the completion within a DispatchQueue.async instead of calling it immediately. With this change we now get a failing test XCTAssertEqual failed: ("42") is not equal to ("0") -, which is what we expect.
Now that we have a failing test to guide us we have a few options to resolve this. Let’s update the test first to what we believe the api should now be in a fully asynchronous world:
finalclassStep4:XCTestCase{functestProcess(){letrandomNumberGenerator=RandomNumberGenerator()randomNumberGenerator.remoteNext={completioninDispatchQueue.main.async{completion(42)}}weakvarcompletionCalled=expectation(description:"Completion was called.")process(randomNumberGenerator:randomNumberGenerator){numberinXCTAssertEqual(42,number)completionCalled?.fulfill()}wait(for:[completionCalled!],timeout:0.1)}}
Lines 11, 15 and 18 are added to take advantage of the asynchronous testing features of XCTest.
Line 13 represents how the new API will look when it has been made asynchronous.
Finally we need to update the production code to make this test pass:
The source transformation here boils down to adding completion handlers to RandomNumberGenerator.populate and process and wiring everything up. With this in place we have passing unit tests.
Conclusion
By accidentally making our asynchronous code synchronous in our tests we are not validating our code in a realistic situation. If remoteNext is performing networking then these tests are actually testing an impossible situation as we would never expect an immediate response.
In the example above we changed the implicit contract of our implementation by accident, which lead to incorrect behaviour. It could have been equally possible that we deliberately made our tests synchronous to make them easier to write. Whilst not adding the expectation boilerplate is nicer it also leads to potentially fragile tests or weakened acceptance criteria. A DispatchQueue.async may not model the production code exactly but it’s better than changing the functions implicit contract and will not add much overhead to a test run.
What do you test when it comes to Swift’s Codable? I start with some thoughts on stuff that might be useful to test and then dive into TDD’ing an example of a custom Decodable implementation.
Test Candidates
Deciding what we could/should test is worth spending some good time thinking about. To start I’d consider the following:
1) How are required properties handled?
I expect that my type will decode successfully if all the required properties are present and any missing optional properties should have no effect.
2) Are source values being used?
I expect that if my source had the value Paul for the key name then I would end up with a parsed Swift type that reflects these values.
3) Can the data round trip through Codable?
If I am conforming to Codable and not just Decodable or Encodable individually then I expect my type to be able to go through both decoding/encoding without losing any data.
Off the shelf Codable
If you are just conforming to Codable without providing a custom init(from:) or encode(to:) then you probably don’t need to add any of your own unit tests to validate the types decoding/encoding directly. Looking at
the list above we can feel pretty confident that the compiler is generating code that handles all of these cases. This is a great place to be - if I have the following type:
Then I have great documentation that states that both height and width are required and attempting to decode from a source that is missing either will behave in a well defined way. Due to the fact that the implementations of both encoding/decoding are being generated by the compiler I can also be confident that it will generate the correct code to create instances that contain the values from the source.
Custom Codable
For cases where I am defining my own custom implementation of Codable I need to test all the scenarios above. Let’s take the example of wanting to be able to decode the following types:
First up notice that Rectangle and Square are both using the off the shelf Codable implementations provided by the compiler so it’s only Shape that we need to write tests for.
Let’s test drive this
Looking at the JSON we can see that there are keys for type and attributes that are not in our native type. Both of these keys are required in order to parse a Shape so we need to write tests to cover this (don’t worry I simplify the following monstrosity a bit further on):
These two tests are verifying that both those properties are required.
NB: I’ve not placed the fixtures within the main body of ShapesTests on purpose to make it so the main body is all about the tests.
I’ve used private let outside the scope of the unit test class instead of in an extension ShapesTests because I find it tends to be easier to maintain. If I nest inside an extension I end up more often than not breaking tests if I decide to rename the test class name and don’t remember to update all the extensions.
To get these tests to pass I need to write a little bit of implementation. The simplest thing I can do is ensure that both of the keys exist without using their values and then create a default instance of Shape.rectangle (because I need to set self to something). I can do this by decode‘ing from the keyed container and assigning to _:
This implementation is pure nonsense but it makes the tests pass.
I’m not happy with the current tests as they are pretty hard to read and are not doing a great job of describing my intent.
Refactoring tests
I want to refactor these tests to make it really clear what their intent is. Tests that are difficult to read/understand are often just thrown away at a later date, in an effort to avoid this I’d rather spend a little bit of time making things clean.
1) Extract a helper for the assertion
That assertion is pretty difficult to read - at its core it is just checking that decoding will throw a key missing error. I create a helper by copying the original implementation into a new function and making the external interface simpler, which as you’ll see below tidies up the final callsite.
funcAssertThrowsKeyNotFound<T:Decodable>(_expectedKey:String,decoding:T.Type,fromdata:Data,file:StaticString=#file,line:UInt=#line){XCTAssertThrowsError(tryJSONDecoder().decode(decoding,from:data),file:file,line:line){errorinifcase.keyNotFound(letkey,_)?=erroras?DecodingError{XCTAssertEqual(expectedKey,key.stringValue,"Expected missing key '\(key.stringValue)' to equal '\(expectedKey)'.",file:file,line:line)}else{XCTFail("Expected '.keyNotFound(\(expectedKey))' but got \(error)",file:file,line:line)}}}
The next thing I would consider refactoring is the fixture duplication. For both of the tests above I am essentially taking a working JSON object and stripping away keys one at a time and verifying that the correct error is thrown. I can leverage some good old fashion Key Value Coding to make this simple helper:
Now that the tests are looking cleaner lets add some more to force the next bit of production code to be written. I’m going to chose to just verify that type is utilised correctly. I have a feeling this test will be temporary as a later test will make it redundant but let’s write it to keep things moving:
functestDecoding_whenSquare_returnsASquare()throws{letresult=tryJSONDecoder().decode(Shape.self,from:fixture)ifcase.square=result{XCTAssertTrue(true)}else{XCTFail("Expected to parse a `square` but got \(result)")}}functestDecoding_whenRectangle_returnsARectangle()throws{letresult=tryJSONDecoder().decode(Shape.self,from:fixtureRectangle)ifcase.rectangle=result{XCTAssertTrue(true)}else{XCTFail("Expected to parse a `rectangle` but got \(result)")}}...privateletfixtureRectangle=Data("""
{
"type" : "rectangle",
"attributes" : {
"height" : 200,
"width" : 400
}
}
""".utf8)
These tests are pretty permissive and will allow anything as long as it’s the correct enum case. The simplest thing I can write to make this pass is to hardcode some random shapes:
At this point my tests are now a bit rubbish. I’ve had to provide a new fixtureRectangle which isn’t actually representing a valid source anymore. It makes sense to remove these tests and write some more reasonable assertions.
I’ll start by addressing squares first:
The tests pass and I didn’t change the current implementation of init(from:), which is slightly worrying as I know I hardcoded values. The best thing to do is to write another assertion with different data:
We’ve now written tests that cover the requirements at the top (for the Decodable half of the Codable story). We’ve verified that required keys are in fact required and checked that when you do use a decoder your newly created types take the values from the source.
Looking at this new code (listed below) I can see that adding all these new tests has gotten really ugly:
My concerns here are primarily based around adding lots of new fixtures that are poorly named. I could improve the naming but then I feel like the data definition and usage is really far apart. I’d most likely still have to go hunting to look at the fixtures regardless of the name.
I can start to tidy this up by inlining square, square2, rectangle and rectangle2 and deleting the constants:
A further enhancement would be to bring the fixture data into the body of the test as well. We could use a similar idea from earlier where we just munge some existing data inside the test body where it is used so everything is kept local to the test. Here’s the helper function, which again leverages the power of Key Value Coding:
Using the new helper gives the following tests. The code isn’t as short anymore but all the data is local to the tests making it easier for future readers to figure out what data is changing to drive the various conditions:
Testing Codable implementations isn’t particularly hard but the boilerplate code required can get out of hand pretty quickly. I thought I’d run through a TDD process to get to the final solution as I find this stuff personally interesting and hopefully someone else might to. Hopefully I’ve highlighted some basic stuff to test when looking at custom Decodable implementations and shown that it’s useful to refactor not only the production code but the test code as well.
Quite the mouthful of a title but nevertheless it’s a typical problem.
Receiving data from a remote service is super common but it’s not always
obvious how to represent our data in a strongly typed language like Swift.
Problem outline
Let’s imagine an example where we are using a remote service that
returns a collection of shapes. We have structs within our app that
represent the various shapes and we want to parse the JSON objects into these native
types.
Our initial attempt to parse this might end up creating a new type called
FeedShape that has optional attributes for every possible shape. We can
use JSONDecoder to parse the feed. Then as a second step we can map the shapes
into our native types. That might look like this:
Whilst this will work it’s really not pleasant to write/maintain or use.
There are many issues with the above:
1) Optionals everywhere
Every time a new type is added that we can support within the app our Attributes struct will grow. It’s a code smell for
there to be a type where most of its properties will be nil.
2) Manually checking requirements before creating objects
In order to create the concrete types we have to manually check the type property and that all the other required properties have been decoded.
The code to do this is not easy to read, this fact is painful because this code ultimately is the source of truth for how to decode these objects.
Looking at the current Attributes type we can see that all it’s properties are Double? - it could be quite easy to copy and paste the property
checking logic and end up trying to use the wrong key across multiple types.
3) Stringly typed code
To create the concrete types we need to check the type property against a String. Having repeated strings scattered throughout a codebase is generally bad form
just asking for typos and refactoring issues.
4) We’ve lost the order
Due to the way the above is modelled there is no current way to keep track of the order in which the concrete types should actually
appear.
5) It’s not taking advantage of our Codable types
Our Square and Rectangle types already conform to Codable so it would be beneficial to make use of this rather than manually
creating our types. Using Codable also resolves the poor documentation issue raised in 2 because for simple types the compiler will generate
the Codable implementation just from the type declaration.
Can we do better?
To make an improvement that addresses 2, 4 and 5 we can deserialise our collection to an [Any] type. This requires a custom implementation of Decodable in which we loop over the items and delegate the decoding to the Shape/Rectangle decodable implementations. The code looks like the following:
Although this is an improvement we still have stringly typed code and we’ve introduced another issue. Now we have an [Any] type. The use of Any can be a smell that we are not modelling things as well as we can do. This can be seen when we come to use the collection later on - we’ll be forced to do lot’s of type checking at run time. Type checking at run time is less desirable than at compile time because it means our app might crash in the wild as opposed to simply not compiling. There is also the issue that there is nothing at compile time that forces us to handle all cases e.g. I could very easily write code like this
shapes.forEach{iteminifletsquare=itemas?Square{// do square stuff}// Ooops I forgot to handle Rectangle's or any other new type we add}
Can we do better still?
The issues above can all be addressed.
In order to resolve 5 we need to create an array that can contain one type or another. Enums are the mechanism
for creating the sum type we need, which gives us:
Issues 1, 2 and 5 can all be resolved by taking advantage of the fact that our types are already Codable. If we
make our new Content type Decodable we can check the type we are dealing with and then delegate the decoding to the
appropriate Square/Rectangle decodable implementation.
NB: This is probably the trickiest transformation to follow, especially if you’ve not worked with custom decoding before.
Just google any API you don’t recognise.
By reifying the type property from a String to a real Swift type we convert run time bugs into compile time issues, which is always
a great goal to aim for.
NB: The Unassociated enum might look a little odd but it helps us model the types in one concrete place rather than having
strings scattered throughout our callsites. It’s also quite useful in situations where you want to check the type of something
without resorting to case syntax e.g. if we want to filter our collection to only Squares then this is one line with our new Unassociated type:
shapes.filter{$0.unassociated==.square}
without the unassociated type this ends up being something like
If you need to represent a collection that can have multiple types then you’ll need some form of wrapper and enums can
perform that duty well when it makes sense.
Swift’s Codable is really powerful and helped remove a heap of issues that arise from manually parsing/creating objects.
Removing optionality, reifying types and using compiler generated code are great ways of simplifying our code. In some cases
this also helps move runtime crashes into compile time issues, which is generally making our code safer. The benefits here are
great and it shows that it’s really worth taking time to model your data correctly and then use tools like Codable to munge
between representations.
The title was a little bit of a lie as I only walked through the Decodable part of Codable (see the listing below for the
Encodable implementation).
Full code listing
The full code to throw into a playground ends up looking like this:
//: Playground - noun: a place where people can playimportFoundationletjson=Data("""
{
"shapes" : [
{
"type" : "square",
"attributes" : {
"length" : 200
}
},
{
"type" : "rectangle",
"attributes" : {
"width" : 200,
"height" : 300
}
}
]
}
""".utf8)structSquare:Codable{letlength:Doublevararea:Double{returnlength*length}}structRectangle:Codable{letwidth:Doubleletheight:Doublevararea:Double{returnwidth*height}}structFeed:Codable{letshapes:[Content]enumContent:Codable{casesquare(Square)caserectangle(Rectangle)varunassociated:Unassociated{switchself{case.square:return.squarecase.rectangle:return.rectangle}}init(fromdecoder:Decoder)throws{letcontainer=trydecoder.container(keyedBy:CodingKeys.self)switchtrycontainer.decode(String.self,forKey:.type){caseUnassociated.square.rawValue:self=.square(trycontainer.decode(Square.self,forKey:.attributes))caseUnassociated.rectangle.rawValue:self=.rectangle(trycontainer.decode(Rectangle.self,forKey:.attributes))default:fatalError("Unknown type")}}funcencode(toencoder:Encoder)throws{varcontainer=encoder.container(keyedBy:CodingKeys.self)switchself{case.square(letsquare):trycontainer.encode(square,forKey:.attributes)case.rectangle(letrectangle):trycontainer.encode(rectangle,forKey:.attributes)}trycontainer.encode(unassociated.rawValue,forKey:.type)}enumUnassociated:String{casesquarecaserectangle}privateenumCodingKeys:String,CodingKey{caseattributescasetype}}}letfeed=tryJSONDecoder().decode(Feed.self,from:json)print(feed)letjsonEncoder=JSONEncoder()jsonEncoder.outputFormatting=.prettyPrintedprint(String(data:tryjsonEncoder.encode(feed),encoding:.utf8)!)