I’m live streaming my programming from start to finish on an iOS app to help folks explore and translate Scottish Gaelic phrases.
The last post about this provided links and director’s cuts for the first five streams, in which we laid out many of the view components. In this post, I’ll cover the streams where we begin adding code functionality to the home view.
We start by fetching posts from a WordPress API. Or rather…testing the fetching code we haven’t written yet.
Look, most tutorials on automated testing don’t reflect the way that folks with years of TDD/automated testing experience use tests in practice. And I think that this plays a big role in why more programmers stop using automated tests.
Tutorials, understandably, want to start simple. So they say “Here’s how we would test this
add() method.” If they wanna get fancy, they show you several tests for the same class, and then they refactor the class and show you that all the tests still pass. Yay!
The implication is that developers should go forth and unit test each method in their class. Where classes interact, tutorials explain how to mock the collaborator classes.
This concludes most automated testing education, and this set of practices works great when we need to ensure that complex logic works inside a given, single class.
That’s like 20% of cases. The rest of the cases fall into one of two buckets:
- False positives: There’s some code in the class, like a constructor or some attributes, but it’s not complex logic. The whole program crashes immediately if this is written wrong. We don’t (at least I am hoping we don’t) ship code we haven’t run, so adding tests for this stuff provides zero benefit. After years of writing hundreds of these tests because “you’re supposed to test everything”, programmers are “meh” on unit tests because such a large chunk of the tests they wrote would never catch a single defect, and they knew it at the time of writing it.
- False negatives: A lot of the risk in a code base lives not in individual class logic, but in the interactions between different pieces of code. Most TDD and automated test resources, in my experience, either don’t discuss this, or they caution that those tests will fail for reasons that have nothing to do with the code. As a result, the first time one of those tests fails when the code is working, the devs remove it or stop paying attention to it.
Cut to this first video.
In this video, you see me start with a big ol’ integration test. This thing could fail for all kinds of reasons that have nothing to do with my code. For example:
- If my internet is out or wifi is turned off, it will fail
- If the WordPress API changes, it will fail
- If gaelic.co deletes enough blog posts that only 1 or 2 are left, it will fail
None of that has anything to do with my code, but I still want this test. Why? Because if any of that happens, the app will break, even though the app’s code isn’t the reason why.
I once saw a day-long bout of customer testing on our mobile interface go sour in the first four minutes over exactly this: the API team pushed backward-breaking changes and didn’t tell the mobile team, who didn’t catch it before installing the app on sample devices because all the automated tests had passed. At the time, I was the anchor for the mobile team. I met with the anchor of the API team that afternoon and we agreed: he should have told us, but I should have caught it, too.
That day I swore, for the rest of my career, I’d do myself a favor and write this test.
In the next stream, I learn more about customizing the way our Swift objects decode from JSON. We get to enjoy the feel-good effect of changing the code and knowing it works because our tests pass, given that our API test from the last stream runs assertions on hydrated objects rather than the JSON itself.*
*Worth noting at this point: I have been writing automated tests at this point for (jeez, time flies) the better part of a decade. So I have some intuition about what level of abstraction to write a test and what collection of behavior to include or exclude. It wouldn’t be a weird choice to only test up to the JSON in this test—I just have a set of experiences that suggests going all the way through to the object, and we saw it pay off in the refactor. One day, if I’m very lucky and work very hard, I’ll figure out how to codify this intuition and save other people the trouble :).
In this stream we also start hooking up some views to code. We learned that my computer doesn’t have the bandwidth to stream, run the simulator, and reboot XCode all at the same time, so I cut the stream for a moment to get everything booted and then:
We finish hooking up our image and label views to our view controller so we can populate them with blog post information.
Then, in the next stream, we start populatin’.
Again, our work begins with a test.
This view controller test is more granular than the API test: we mock out the BlogPostService with some mock blog posts and check to make sure that the titles go where they’re supposed to. I explain now I handle dependency injection in a framework where I don’t have control over instantiating my own dependent objects.
I also talk about where I don’t use a unit test. I know testing images to be cumbersome in iOS, and I also know that it’s the kind of thing we’re really likely to manually QA anyways, because who doesn’t love seeing a pretty picture? So instead of an automated test to make sure this code is working, we choose another risk mitigation strategy: a sensible default, so that the app looks okay even if the image doesn’t load, or isn’t there.
This is the other thing that I think a lot of automated testing resources miss: they don’t situate automated testing within the suite of things you can do to make sure an app works. This is something I’d like to change, and I’m working on (slowly). Look for more of that in future stuff from yours truly 😉.
If you liked this piece, you might also like:
How to Jump-Start a New Programming Language, or maybe, even, gain a more concrete mental model of the one you already use!
Lessons from Space: Edge-Free Programming, which also explores an Android app’s design and the engineering choices behind it (plus, cool pictures of rockets!)
How does git detect renames?—This piece is about how git detects renames, but it’s also about how to approach questions and novel problems in programming in general.