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:
from JSON that looks like this:
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.
The tests now become:
2) Extract helper for munging JSON
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:
With the above refactorings the entire test file now looks like this (assuming the helpers were placed in new files):
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:
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:
This gets us back to a broken test as the hardcoded values I return no longer match those I expect with different fixture data.
Making the parsing code work is just a case of updating the case "square" logic in the init(from:) func:
Let’s repeat the above to get Rectangles working as well.
Add some tests:
Then updating the parsing code becomes:
Recap
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:
For reference the full code listing after all of the above is:
Shapes.swift
ShapesTests.swift
Conclusion
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.