Test Driven Development
Telling a story
About a year ago I switched my job. I've entered a team which was about to start a new project and never applied TDD in iOS. As I'm a strong supporter of TDD we sat down once a week for a few hours and worked on a pet project to develop the skills and discipline necessary for this. It wasn't an easy way and often I was telling my colleagues to write the test first. A few months in one colleague said:
"I can't image writing code without having a test first anymore."
Hearing this made me very happy, thus I've decided to help you along this way.
The Cycle Of TDD
TDD is a very simple but powerful technique. It consists of 3 steps:
- Write a minimal test, which fails (red)
- Write the minimal code to fulfill the test (green)
- Refactor (green)
This is most often the hardest part in TDD. Developers tend to test too much, or too specific. We have to couple our tests loosely with the module we are testing, but at the same time, the test has to be specific enough, to actually just test a small part. I guess the worst (and most common) mistake is testing implementation details and not behavior. If you want to calculate the square root of 9, it does not matter how the module does it, as long as it returns 3.
Sticking to our above example of calculating the square root. Writing the first test, we pass 9 and expect 3 as a result. So whats the minimal code, we can write to pass this test?
That's it. As long as we don't have a test, requesting something else, we are done. There is no over-engineering for every possibility. We add the code we need, when we need it. Not a second earlier. This does not stop you from thinking ahead.. instead it should make you aware of your tests driving your architecture. They are the first consumer, and if it is hard to write a test, it's hard to use your code.
Never forget this step. This step keeps your code clean. Not only your code, but also your tests. After every trip in the TDD cycle, you should refactor and check whether your code or your tests need refactoring. Just always stick to one OR the other. Never change both at the same time. And the only other condition is: Do not break anything.
To continue my story of introducing TDD to my team, there was a remark which stuck to me quite some time:
"I really want to see what happens at a major refactoring. Before that I don't know what I should think about TDD".
You have twice the amount of code compared to just writing production code (actually studies showed about thrice the amount of code Test : Production is 2:1). This would mean a refactoring will also affect tests and code at the same time. How we do this? I've already mentioned the keypoint for tests above. Test behavior, not implementation details. This is about 90%. My approach depends on what I'm doing:
- Changing the algorithm? Just do it, the tests don't change!
- Creating an entire new architecture? Then I create new files with TDD and switch the implementation at some point. I have multiple tests for the same feature, but keeping the old tests till the switch shows me, if I forgot something. A broken test after the switch is not necessarily a bug. You have to look at every test, but this will reassure you, you did the right thing.
When writing code, you have to write documentation. There are comments, wiki entries, API documentation or even large handbooks. Sadly documentation tends to get outdated, as the code changes. Developers are lazy and even the most reliable developer can forget to update every place this one specific detail was documented. TDD helps with documentation, as it describes the current state of the code. Having the tests named in a reasonable way, new (and old) developers can just read the name and know what the test and the software are supposed to do. As long as you keep running the tests, they are a source of documentation, which never gets old. I've experienced quite often a product owner declaring something as bug. Later an experienced developer mentioned this was a requirement. TDD will make sure to document the requirements of the time writing the tests.
TDD in iOS
When I started writing tests for iOS, I didn't know where to begin. In Java there was no problem, but for some reason, I just didn't get the hang of it in Objective-C. How do I test the delegate methods of an UITableViewDelegate? Or how do I test UIKit stuff?
The answer to those questions is quite simple. If it's a method, you can call it. If it's not your code, you don't test it. Apple is responsible for testing UIKit, why should you do their job? Just test, what you can test, but don't forget, you are responsible for your code, not for everything.
Often people tend to see the initial time to finish a feature, as the only time the developers have to spend on this feature. Maintenance is not something they account for. This results in the Product Owner complaining about being to slow, and shareholders need to see value, which they don’t see in test. As a developer you will have to fight battles defending your process. Do not give any leeway . Yes the initial development of a feature takes longer, but in the long run, you will be faster and more confident changing your code.
Being slow in the beginning, not having features as fast as possible… why should shareholders like the idea of TDD? They don't. But there is still value. I know companies, having the shareholders test your software by hand. Reducing the time necessary for this, they might support it in the long run. If you're lucky and they realize what you are doing to help them. Communicate it, and make them realize! Tests are not only for you, but also for everyone interested in your software.
So what is TDD all about? Having many tests? I don't think so. It is about feedback. Is the code I've written just now correct? Did I break anything? A broken test is not necessarily a bug you introduced. Maybe your changes do not fit into the app. The test is just feedback and as such you should think about, what this tests tries to convey to you.
We will continue with TDD in our project. Hopefully this small excursion improves your testing. In later posts I will provide some techniques for writing tests (and mocks) in iOS.
Previous: Git Hooks
Thank you for reading! If this post is helpful to you, please consider supporting me through Patreon: https://www.patreon.com/jan_olbrich