It’s a high compliment when users ask for new features. They’re saying they can get even more value from our software if it did even more. We want to satisfy that kind of request.
If software is used it will, inevitably, need to be changed. But software isn’t always written in a way that makes it easily modifiable, so it turns out to be difficult and expensive to add new features. How expensive? A National Institute of Standards and Technology study found that it costs twice as much to make minor enhancements to a system than it cost to build the system in the first place.
Why is it so expensive to add features to existing software? In a nutshell, it’s because we don’t value writing changeable code, or we think that writing changeable code will take much longer to write. Changeable code is software that when we go back to add features, it doesn’t cause other parts of the system to break. When working with changeable code, it’s cost-effective to go in and add features.
Writing good, changeable code doesn’t take longer, but it does require paying attention to certain things when building a system. First and foremost, we want to make sure our system is understandable. We want to have a strong metaphor for the system we’re building, one that reflects how we interact with it.
Good design patterns, principles, and practices can help as well. If we use abstractions and encapsulation to hide variations and other implementation details so they’re straightforward to change later, we can drop the cost associated with changing existing software and make it far more economically feasible to add features to existing systems.
Understanding the importance of testability and code quality is critical, too. Creating independent, testable behaviors in the system allows us to write automated acceptance tests so we can automatically validate that our software still works as expected.
One very important aspect of writing changeable code that is often overlooked is having a good suite of unit tests that support us in refactoring the code when needed. Unfortunately, the way most developers practice test-driven development (TDD), the unit tests they write get in the way of refactoring rather than providing a safety net. Features are often intertwined with other code, making it difficult to test independently. Then, when one piece of code is changed, it can cause bugs in seemingly unrelated parts of the system.
To prevent these types of errors, test all the features of a system when any change is made. But if testing is a manual process, that will get expensive and will tend to be put off until late in a project’s lifecycle. So instead, adopt automated testing, which can be done continuously. Automated testing reduces the impact of making changes to a system by quickly identifying hidden dependencies that can then be resolved as they are being discovered.
When doing TDD, we want to write tests against behaviors, not the way we implement those behaviors. Unit tests should treat behaviors as a “black box” and not depend on the way those behaviors are implemented. When we test behaviors instead of the way we do those behaviors, it gives us the freedom to refactor our code and change those implementations without breaking our tests, as long as the behaviors themselves don’t change.
When we use unit tests and TDD to specify behaviors, they become a form of documentation that shows us how our behaviors are consumed. This reduces the need for internal documentation. I think of my tests as living documentation because I can run my tests and prove they’re up to date. With a specification document, I’m never sure if it’s up to date or not.
Using TDD to specify behaviors can be very helpful in keeping us focused on fulfilling acceptance criteria, writing independently testable code, and providing living documentation, but TDD doesn’t replace quality assurance (QA). We still need to go back, think about what can go wrong, and write additional tests to cover those situations. We often need to cover aspects of our code that aren’t covered by our unit tests. This can include functional and integration tests as well as testing for security, performance, and some edge cases.
We don’t want developers to do this kind of testing when doing TDD. The QA mindset is to look for things that could go wrong, but that isn’t the TDD mindset. It’s the same for writing prose. If you’ve taken any kind of writing class in school, then you’ve probably gotten the advice not to edit yourself as you’re writing. That’s a recipe for writer’s block. It’s better to write without worrying about spelling, grammar, etc., and then go back later and edit your work.
When developers “put on their QA hats” when doing TDD, it’s easy to get stuck. They can write too many tests and implementation-dependent tests that make it harder to refactor code in the future. TDD can help development in many ways, but it doesn’t replace QA.
Having a suite of regression tests built using TDD helps QA because they don’t have to spend their time manually doing regression testing. Repeated manual testing of code is highly inefficient and something we want to avoid. The software industry is all about automating other industries. We should take our own advice and automate the parts of our industry that can be automated.
Writing changeable code isn’t difficult or time-consuming. We just have to understand some basic object-oriented design skills and how to build code that’s straightforward to change. A focus on quality can take some effort to learn and skill to apply, but it’s worth the dedication.