How do you use tests to drive writing logic to decode JSON data into a model object?
By feeding the tests a predefined JSON input and verify the decoding produces a value matching it.
Swift’s Decodable protocol offers a type-safe way to describe how to transform Data, such as a JSON in the body of an HTTP response, into an object or value type. With Decodable doing most of the heavy lifting, is it worth using tests to drive the implementation of JSON decoding logic?
When the JSON object has keys and values with names and types that map directly to Swift, there is no real logic you need to write to decode it. In such a case, you may feel comfortable writing the source code directly, without the help of a unit test. If the JSON object is elaborate or if you want to decode it into a Swift type with a different structure, then a test will help you along the way.
Choosing whether to deploy TDD to implement JSON decoding logic is a matter of tradeoffs. Is the extra time you’re spending writing the test worth the feedback it gives you? Once the test is in place, will it guard against regression that may otherwise go unnoticed?
To answer those questions, you first need to know how to write this kind of tests. In this chapter, we’ll explore two different techniques to write tests for JSON decoding and reflect further on the return on investment of applying TDD to this part of the app’s implementation.
We’re moving along steadily in making our app fetch the menu from a remote API. In the previous chapters, we built the view part of this new functionality. It’s now time to take one step further down the stack, coding the conversion from the raw Data a network request would return into the MenuItem domain model.
Swift provides a convenient way to decode Data into a different type via the Decodable protocol. Our job is to conform MenuItem to it and verify the decoding is successful.
How do we go from that JSON to a MenuItem using TDD? As always, we start with a test. In this instance, we’ll write a test for the decoding behavior.
How can we arrange the JSON input for the test? Let’s see two different approaches.
Option 1: Inline Strings
The test builds now. It also passes already because the names of the properties in MenuItem match the names of the keys in the JSON input, so the compiler takes care of all the decoding for us.
The fact that the test passed when the only change we made was to conform to a protocol without any extra code raises a question: are we testing our code’s behavior or that of Decodable?
As mentioned in this chapter’s introduction, there are instances in which using a test to drive the JSON decoding implementation may be an unnecessary overhead. We’re learning how to write this kind of tests to discern when they are useful and when they aren’t. Let’s look at another option for the Arrange phase and then discuss when using TDD for JSON decoding is appropriate.
Option 2: JSON Files
Another option to provide the input for our test is to have JSON files holding the input values.
- 1.
Select the test target folder in the project navigator.
- 2.
Right-click it and click “New File…” or press Cmd N.
- 3.
Select the Other ➤ Empty file and then click “Next” or press Enter.
![../images/501287_1_En_9_Chapter/501287_1_En_9_Fig1_HTML.jpg](https://imgdetail.ebookreading.net/202109/3/9781484270028/9781484270028__9781484270028__files__images__501287_1_En_9_Chapter__501287_1_En_9_Fig1_HTML.jpg)
The Xcode New File… dialog, filtered to show only empty file templates
- 4.
Add the .json extension to the file name and select “Create” or press Enter.
![../images/501287_1_En_9_Chapter/501287_1_En_9_Fig2_HTML.jpg](https://imgdetail.ebookreading.net/202109/3/9781484270028/9781484270028__9781484270028__files__images__501287_1_En_9_Chapter__501287_1_En_9_Fig2_HTML.jpg)
The Xcode dialog to name and add a new file to the file system and project targets
- 5.
Populate the file with the JSON for the menu item:
Alternatively, you can create the JSON file with your favorite text editor and save it into the test target folder. From Xcode, you can then right-click the test target folder and select “Add Files To ‘<test target name>’…” or press Cmd Alt A to reveal a file navigator where to select your JSON file.
If you haven’t removed the source changes we wrote in the previous example, this test will immediately pass.
Which Option to Choose?
The advantage of using inline strings is that you can keep the input values close to the expectation they support. With the JSON file option, it’s unclear where “a name,” “a category,” and true come from, the only way to be sure is to check the JSON file as well.
On the other hand, using JSON files makes our tests tidier. As your input JSON grows in size, the inline string approach will result in longer and longer test methods, whereas tests loading the data from a file won’t need more input setup code.
Using JSON files is also useful if you can automate generating them from your API, making sure the decoding is up to date without putting in place a full end-to-end integration test suite.
Which option to choose depends on your domain model. If your API has many entities, each rich with properties, then using files might make it easier to navigate your test suite.
Unless you are building an app against an existing API and already know what you’re dealing with, I recommend starting with inline strings and optimizing your tests’ understandability.
Now that we’ve explored the how of JSON decoding testing, let’s take a step back and look at the when. When is TDD useful to implement JSON decoding?
Is Testing JSON Decoding Worth It?
It depends. Like everything else in software development, it’s a matter of tradeoffs.
In the tests we wrote earlier, most of what we’re testing is the behavior of Swift’s JSONDecoder. That’s code that we can’t control, and, since it comes from the Swift standard library, it’s safe to assume it works as advertised.
If you’re doing something nontrivial in the decoding, then a test to make sure everything is working correctly is valuable.
To decode this JSON into our MenuItem object, we need some extra code on top of what the compiler synthesizes for us via Decodable:
The error is dense, but by reading through it, you can understand that the decoding was looking for a String for the “category” key but got a dictionary instead. That’s exactly the error we’d expect from the change we made in the JSON input.
In the case of a nontrivial JSON structure like the one in this example, I feel a test is a useful feedback mechanism to guide the decoding implementation.
Something else to keep in mind is that we always call JSON decoding as part of a network request or some other kind of data transferring; it’s not something we might call directly in a ViewModel. Because the return type of the fetchMenu in MenuFetching is AnyPublisher<[MenuItem], Error>, a component fetching the menu from the remote API will also have to decode the received data into a [MenuItem]. As long as the decoding has no custom logic, the tests for the networking component will fail if something goes wrong. That is, testing the networking code implicitly exercises the JSON decoding logic.
If your API is such that you can decode it with little to no custom code, you’re better off jumping straight to implementing the networking code and saving yourself the time to write tests for the decoding logic. That’s because the Swift standard library takes care of the decoding; it’s not your responsibility to test that.
If there is a certain amount of ad hoc decoding logic, though, like in the previous example, then a set of focused tests will help you implement it and make sure it doesn’t break.
A TDD purist would be pulling their hair out and scream at me in frustration right now. How dare I suggest that it’s okay not to write tests in a book about Test-Driven Development?!
Test-Driven Development is a practice, not a way of life. If the return on investment of writing some code test-first is not worth it, both in the short and long term, then it’s okay not to write a test for that code.
We’ve wrestled with when deploying TDD is appropriate for writing JSON decoding logic and seen a couple of options on how to do so. It’s now time to put this logic in action by building the final component in our feature: the one responsible for fetching the menu data from the API.
Practice Time
Key Takeaways
One approach to test JSON decoding logic is to define an inline String, convert it to Data, and use it as the input for JSONDecoder. This approach is useful for input JSONs that have few keys and to make the input value to model property relationship clear.
Another option is to define the input in a dedicated JSON file and convert that to Data in the test. This approach is useful for input objects with many keys but adds a layer of indirection between the input and the resulting SUT properties.
Keep your tests tidy by extracting the logic to convert from JSON to Data into helper functions.
Testing JSON decoding logic is valuable if there is custom logic required to conform to Decodable. If all the model properties match the input JSON’s keys, Swift takes care of everything for us, and a test for it is redundant.
Endnote
- 1.
There are options other than REST for an API backend. Two that are gaining more and more popularity are GraphQL and ProtoBufs. Both of them add a type-safety layer to the API, which you can – and should – leverage to code-generate all the conversion logic between API responses and application domain objects.
ProtoBufs serializes data in a custom format that is more efficient than JSON. GraphQL responds with JSON objects, but stands out for its query language. Rather than having different endpoints for different resources, you ask a GraphQL backend for data by sending it a query that describes the information you need.
In a small application like ours, the overhead of adopting either of those solutions dwarfs the benefits we would get in terms of maintainability and efficiency.