When you’re new to Test-Driven Development, there’s a strong tendency to continue writing code the way you used to. Then you step back and ask, “But how can you unit test this? TDD is so impractical!”
Almost always, my answer is: “Then don’t write it like that.”
Apple recently published a blog post, Working with JSON in Swift. I was looking for tips on Swift JSON parsing, and found the beginning of the article quite helpful. But it took a bad turn at the end…
Apple shows us how not design code
The article explores various aspects of JSON parsing. But then there’s a section titled, “Writing a Type Method for Fetching Results”:
You can create a type method on the Restaurant structure that translates a query method parameter into a corresponding request object and sends the HTTP request to the web service. This code would also be responsible for handling the response, deserializing the JSON data, creating Restaurant objects from each of the extracted dictionaries in the “results” array, and asynchronously returning them in a completion handler.
My goodness. How many responsibilities can we cram into a single class? Here’s how Matt Diephouse counts it:
`Restaurant.restaurants(matching: completion:)` does:
3. URL construction
— Matt Diephouse (@mdiep) September 12, 2016
How would you TDD such a thing? …You wouldn’t!
TDD and the Single Responsibility Principle
TDD gives many kinds of feedback. One of them is, “How hard is it to write a test?” If a unit test is hard to write, it’s likely that the design is wrong.
That doesn’t mean we don’t think hard about how to tackle a problem! But after wrestling with some design ideas, writing tests should be much, much easier. Your TDD will improve as your design sense improves. That’s why I’m currently reading Practical Object-Oriented Design in Ruby by Sandi Metz — I want to continue to improve my design sense. (Disclosure: The book links in this post are affiliate links. If you buy anything, I earn a commission, at no extra cost to you.)
Of the SOLID principles, the Single Responsibility Principle is my closest advisor. How can we TDD JSON parsing of a networking response? Here’s a list of the responsibilities:
- Parse the JSON into a Response Model
That’s it. I’m not even concerned with creating my Entities yet. For the TDD sample app which fetches Marvel comic characters, this means I’m not directly creating any Character objects. I just want to package the response.
Creating an agnostic Response Model
A Response Model is the inverse of a Request Model. It’s a representation of what comes back “from the other side,” whatever that other side is. It could be JSON, it could be XML, it could be a Protocol Buffer. The rest of the app doesn’t know or care.
I once worked on an app that tightly coupled its domain Model objects to XML. The old services spoke XML, so it seemed natural to the developers to build XML parsing directly into the Models. …Until one day, I had to work with a new service. It returned an existing Model type, but spoke JSON instead!
Yeah. Don’t add serializing or deserializing to your domain models. It’s not their job.
Clean Architecture takes decoupling even further. The objects just to the right of the dashed lines defines a Boundary. The Gateway is a protocol. The Request Model is input to the Gateway. The Response Model is its output.
To the left of the dashed lines is the actual Service I’m currently TDDing. It takes care of communicating with the Marvel Comics API.
It can be replaced. Maybe DC Comics comes out with their own API. As long as we’re able to map DC’s response into our Response Model, the rest of the app won’t care.
New possibilities for integration testing
This also opens the door to Not Quite End-to-End Testing. The Gateway can be implemented by a stub service that offers canned responses. You could do UI testing with zero network latency!
Let’s flip this on its head. Say we use these same principles, but isolate the UI instead. The core of our app would work with Commands, not view controllers. We could then do Not Quite End-to-End Testing of the actual networking layer, without involving the UI.
I’ve never seen any program written like this, but I’ve wondered about it for years. I want to head this direction with the TDD sample app. How feasible is a Command-based architecture in a View Controller-centric world? Let’s find out! (Thank you for putting up with the mistakes I’ll make along the way, as we learn together.)
Testability ⇄ Design sense
Design sense and testability are intertwined. Building with testability in mind naturally decreases coupling. Learning to see through the glasses of the Single Responsibility Principle increases cohesion. The concepts of coupling and cohesion go back at least 45 years. They’re worth our continued attention.
And the easiest way to build in testability is with TDD. Pay attention to the different forms of feedback TDD gives. It’s not just about passing the tests (after seeing each one fail). TDD is a cognitive process assisted by automation. When a test is hard to write, ask yourself why.
Take a look at your response parsing. Is it tightly coupled with your Models? Does it have unit tests? Are those tests simple?
[This post is part of the series TDD Sample App: The Complete Collection …So Far]
Practical Object-Oriented Design in Ruby by Sandi Metz