In this episode of Semaphore Uncut, Jon Reid, leading expert in unit testing and test-driven development (TDD) for iOS, talks about the importance of shaping production code in response to tests.
Jon is a Technical Agile Coach and the author of iOS Unit Testing by Example, XCTest Tips and Techniques Using Swift book.
- From Objective C to Swift
- Rapid test-driven development in iOS
- Fewer end-to-end, more unit-tests in iOS
- The real meaning behind the Testing Pyramid
- Creating anti-fragile tests
- F-I-R-E: Understanding unit testing dependencies
- Test-Driven Development as a practice
- Bringing yourself to your code
Listen to our entire conversation above, and check out my favorite parts in the episode highlights!
Like this episode? Be sure to leave a ⭐️⭐️⭐️⭐️⭐️ review on the podcast player of your choice and share it with your friends.
Darko (00:02): Hello, and welcome to Semaphore Uncut, a podcast for developers about building great products. Today I’m excited to welcome Jon Reid. Jon, thank you so much for joining us.
Jon: Thank you for having me, it’s great to be here. For folks who don’t know me, I am an iOS developer who happens to be, what we used to call in the old days, test infected. Test infected means that at a certain point in my career, I was coding without writing unit tests for a long time. Then I came into the world of unit testing, and it infected me. It changed how I think, how I operate and pretty much turned everything upside down. And from then on, I’ve been a dedicated TDDer, test-driven development. I think of myself, in a way as one of a few, not many voices, in the iOS world.
I know that in other parts of the tech community, people are more familiar with unit testing. Less so in the iOS world. So, I started a consultancy, Quality Coding, I offer training and coaching to companies around the world. I help their iOS teams come up to the next level, to improve their ability to do unit testing and TDD and refactoring. And just tightening their feedback loops so that they can operate more efficiently. And as part of that, I ended up writing a book, iOS Unit Testing by Example, XCTest Tips and Techniques Using Swift.
From Objective C to Swift
Darko (03:53): In the beginning, all iOS apps were developed in Objective C, which is quite an old language and software stack.
Jon: I think that actually, having its roots in Objective-C was what made so much unit testing possible and much easier to do. Because it’s a fully dynamic language, if you want to swap out troublesome dependency, it was easy. And there are various tools. I wrote some tools to help folks do that in a, as you said, an old language, 30 years old. It has a lot of history. Coming out of the small talk world, it’s fully object-oriented, except, of course, for the C part.
That’s where Apple when they announced the Swift programming language. The catchphrase they used, was that it was Objective C without the C. Which in some ways it is; in other ways, it is quite a different world and more strict. Some things about that strictness are helpful around type checking, other things are less helpful, but that’s never stopped me from unit testing. I mean, I was doing unit testing long before iOS came around, so it just seemed natural to continue.
Rapid test-driven development in iOS
Darko (10:42): We are advocating that your feedback loop on our test unit suite should be under 10 minutes. Dave Farley, the author of Continuous Delivery book, I think he pushes for five minutes, but we try to be maybe, a bit more realistic and what matches the majority of the teams.
Jon: That would be challenging for an iOS project because of a couple things. One is that it’s fully compiled, so before you run tests, you have to build the thing, and that takes time in itself. But Objective C is a very simple language. You have objects that send messages to objects, and that’s basically it. Swift has a lot more intricacy and complexity to it, and that means that the build time just takes longer, every iOS programmer is familiar with this. So yeah, you have to factor that in.
One key difference in how CI feedback differs from the TDD cycle is that in TDD, you would be working with incremental builds. You would keep most of your object files, your compiled files the same, but only rebuild the one thing that you changed and maybe anything that depends on it, to try to get the feedback as fast as possible. But in CI, as you know, you don’t want to do that. You want to start from a nice, clean, safe starting point that’s predictable so that your results are predictable.
Fewer end-to-end, more unit-tests in iOS
Jon (13:19): Now, one thing about iOS work and automated testing in iOS is that while projects you create in Xcode by default come with various testing abilities built-in, I think that unit testing is still underused, it is underexplored. People tend to fall back on what they call UI tests, which are fully integrated tests that poke the system through the user interface without changing anything else about it. You take your whole app as it is, and you don’t get to change anything in there; you just have to run your app, which conceptually sounds easy. It’s easy to understand that, “Oh yeah. It’s like somebody using your app.”
The problem is that, when people use that as sort of their main testing mechanism, it’s slow for starters. If you think about how you want to test a particular screen, you must first launch the app to get to that screen. Then you have to, most likely, login, right? And then you have to navigate to that screen, tap, tap, and all these things. There are tricks to speed up the transitions and so forth, but it takes a while just to get to the screen you want to test. And even once you’re there, it comes with the built-in dependencies, it’s not easy to replace things with fakes.
And that’s the real strength of unit testing and where I would encourage any iOS folks listening, most of what you want to do, you can do with unit tests. There are tricks, which is why I wrote my book. But the benefit is that your tests are so much faster and they’re more robust, and they’re more reliable, they don’t break as often, and so you get the faster feedback. Even your CI feedback will be faster if you lean more towards unit tests and have fewer UI tests.
The real meaning behind the testing pyramid
Darko (15:28): That balance is something that you for sure know about, is the testing pyramid?
Jon: I think, a lot of people don’t understand about the testing pyramid, and the reason it’s shaped the way it is. It’s not just like a catalog of different types of tests you can write with lower-level ones at the bottom and the higher you go, the more integrated they become. It’s that the shape is wide at the bottom and skinny at the top. Meaning that you should have many unit tests, low-level tests. And the higher your tests go in terms of integration, the fewer you should have. Until finally, maybe you have just a handful of tests that exercise your system fully end-to-end.
Creating anti-fragile tests
Darko (18:34): What is your experience in terms of investing in maintenance of the test suite?
Jon: Sort of to balance what I said, just to be clear, I’m not advocating, not writing integrated UI driven tests, they are important. And there are ways to write those that are less fragile. Folks in the web world know about page objects. In the iOS world, that’s less familiar; basically, it’s another layer of abstraction you write. So that instead of saying, “Find the button with this label and tap it.” Instead, you turn that into a command so that you only have to change it in one place if anything about the label or the button changes. So, there are ways to make your integrated tests less fragile.
I’ve gotten essentially to where I am by concentrating solely on unit tests. I’m writing UI tests now for the first time in my career, this may shock some people. But the reason I haven’t had to is because I haven’t felt the need to, because I’ve had such confidence in the unit tests that I write. And I think that for folks in the Apple ecosystem, in particular, the great barrier to unit testing is not understanding which dependencies create problems for testing.
You mentioned networking, for example, that there ought to be some way of saying, “Let’s not talk to the real network. Let’s talk to this fake thing that’s going to return a fake response.” Or maybe even just stop and capture the request and say, “Here’s the request. You can look at it.” There are various types of things, anything that’s slow. I took an idea about unit testing called the first tests, and I took the first three letters, F-I-R, and added an E on end.
F-I-R-E: Understanding unit testing dependencies
Think about for fire, F-I-R-E, what are the dependencies that cause problems or those that don’t.
- If they are fast, then it’s probably pretty good.
- If they are isolated, that is, they don’t create any side effects that will cause problems for tests coming after it – maybe this test is fine, but you’ve now altered the global state somehow. If it’s isolated, it’s fine.
- If it’s repeatable… time is another one. Anything that operates with dates – isolate it, find a way to not talk directly to the system clock. Instead, talk to an abstraction so that you can test, say, with any times you want to test with.
- And the last is easy. What I mean by that is that there are certain APIs, where if you call a method in the API, it’s not clear how you can check if you called it correctly. If you get a return value, well, you can check that, or if it changes a property, changes a field in the object, you can look at that. But if it does something quietly, some side effect in the background that you have no visibility into, that’s a problem.
Anything that violates these things, if it’s not fast, isolated, repeatable, and easy to test, then you should find a way to isolate that, create an abstraction, usually a protocol or something, that you talk to, instead of talking directly to the thing. And that’s really the key trick to making things easy to unit test.
Test-Driven Development as a practice
Darko (22:41): During the prep part of the call, we mentioned TDD as a practice. And you mentioned that part of your work is to spread that practice and help people adopt it. Can you maybe share a bit on that?
Jon: I think there’s a misconception by a lot of the tech community in general that TDD just means writing your unit tests first. And it’s much more than that. In particular, for me, the key entry point, the thing that made me test infected, was the ability to refactor. And refactoring is another thing that I think the tech community widely misunderstands. People think it means I’m just changing things, or I’m adding a feature. No, it really needs to be moving in small verified steps, getting that feedback as fast as you can about the correctness of a thing.
What TDD does is that it builds that into the three-step cycle so that it actually changes how you code. Instead of coding a bunch of stuff, you’ll end up moving in smaller steps than you can imagine.
The other part of test-driven development that is different than just writing tests at some point is that it’s evolutionary, the design changes and shifts as each new test adds a constraint. I think of it in terms of how to make a statue of a horse from a block of wood. And the answer is, you remove everything that’s not the horse, right? But how do you do that?
You do that in large steps first. You say, well, it has legs. So let’s carve out a large chunk underneath that’s not going to be legs. And maybe we just end up with a big U shape. Now that’s not a horse, but it is a step towards a horse. And then we remove things, each test adds a constraint that narrows the scope of what the code does. Finally, in a way, the horse emerges.
Bringing yourself to your code
Darko (25:38): I remembered how I got into TDD. And to be honest, I had a hard time embracing it before I started doing Katas. And one thing that I was amazed by is that I ended up creating solutions with less code when I fully, or let’s say to a larger extent, embraced TDD. As you said, it’s driving you to do exactly what is described by a test and not more. And I remember, I usually wanted to do more.
Jon: Skillful programmers can code a lot in their heads. It’s very hard for them to say, “No, you’re going too far”. Do that one thing that the test is asking for and now reshape the code. So that it’s more expressive, and it’s less wasteful. It’s clean and easy to maintain, and problems of code coverage go away. Problems of maintenance go away. It’s a great way to work, but I do recommend code Katas as a great way to start.
I teach people, don’t start in your real code because it comes with a mess, it also comes with old habits. And part of what we need to do is learn new habits. And so, the Kata is an exercise where you say, “Okay, I’m going to code this thing, but I’m going to follow these funny rules.” It’s a fun way to learn, and it’s a safe thing because it’s outside of your code. You grow, you change, you become a better programmer, and then you bring yourself to your code.
Darko (29:03): People can find your book on Pragmatic Programmer. Am I correct?
Jon: Yes. Go to pragprog.com and it’s there.
Darko (29:27): Okay, Jon. Thank you so much for joining us and hope you had a great time.
Jon: Thank you for having me. This is a lot of fun.