We’ve talked about how Clean Code leads to better naming and focused functions. Both have a huge impact on how quickly the development team can respond to change. But there’s another piece, without which, the most readable and understandable code still can become a maintenance burden, and that’s testing.
Without testing, even Clean Code – is doomed to failure.
You might wonder why we wrote about Clean Code at all if without testing it’s all for naught. It’s a fair question to ask. Testing is after all a challenging topic in and of itself, one that fills shelves of books, let alone blog posts.
The answer is, Clean Code makes testing both easier and compounds its benefits. Today we’ll talk about the symbiotic relationship Clean Code has with testing, and how you can leverage both for your benefit. Before we get there, let’s review three primary benefits of testing:
- Tests catch errors – testing identifies incorrect functionality
- Tests improve design – testing provides a safety net so we can change how we meet user needs without introducing bugs (for instance, to improve performance)
- Tests are examples of functionality – testing demonstrates unambiguously what our system does (either the test passes or it does not)
These alone are fantastic benefits, but when testing is paired with Clean Code, the synergy between them leads to a whole that is truly greater than the sum of its parts. We can reap both the confidence that comes with a robust test suite and the maintainability Clean Code offers. Let’s see how.
We don’t just catch errors, we resolve them faster
Identifying errors is good, but ultimately we need to fix those errors as soon as possible. Doing so keeps the project schedule predictable, and allows us to remain focused on delivering better capabilities to the users. Testing alone isn’t enough. Once we identify an error with a failing test, we can’t start fixing it until we understand why the test is failing.
Enter Clean Code…
We discussed how Clean Code strives for small units of functionality – functions for instance. That means when we test that function, we have less functionality to verify, which means our tests are easier to write. Ideally, this leads to tests that have fewer reasons to fail. Fewer failure conditions mean a developer can identify why a test is failing and get to the business of resolving it faster.
Furthermore, Clean Code holds tests to the same standards as production code – they are just as valuable. That means we keep our tests readable such that there’s less time spent understanding them when they fail.
Speaking of failure, for any test you write you should always see it fail. Make it fail right after you’ve written it. What diagnostics are provided? Are they clear? Do they lead you towards how to resolve the failure? Write the diagnostics you wish you had, keeping them clean and concise as well.
Summarized – Clean Code paired with testing delivers maintainable tests that provide fast, reliable feedback that help you resolve errors.
- Try writing tests with one and only one reason to fail
- Apply Clean Code techniques to your tests the same as you would your production code. Use good names. Extract methods to keep tests small. If it takes even a minute to understand the test it’s not clear enough.
- See your tests fail when you write them. Make sure the failure diagnostics are clear and helpful.
We don’t just improve the design in the future, we start with a better design in the first place
While technically you can write software tests at any time, Clean Code encourages the practice of test-driven development (TDD), which means before you write a piece of production code, you start by writing a failing test demonstrating what you need that code to do. Summarized TDD has the following steps:
- Start writing a test to demonstrate the needed functionality
- Stop whenever the test fails (compilation failures included)
- Resolve the failure with as little production code as possible
- Repeat until the functionality is demonstrated and implemented
- Refactor to clean up the test and production code to Clean Code standards, including diagnostics
This achieves a superior initial design in two ways:
First, our design is inherently testable because we’ve already written the tests. How many times have you tried to write tests for existing production code and found it difficult. I know I run into it all the time. Let’s tackle that problem head on once and for all by writing our tests first. Many also believe this approach leads to a more modular and just plain better design. Why?
By writing the tests first we’re forced to design our interface first. Our test has to call the desired functionality. What methods will it call? What will they be named? What arguments will they take? We’re forced to answer these questions before we start to think about implementation because we haven’t started implementing the functionality yet. In the words of Kent Beck, the father of TDD, “we imagine our perfect interface” and work backwards from there only as necessary.
Who wouldn’t want to start with an easy to use interface with 100% test coverage? That’s what Clean Code and TDD aim for.
- Read up and practice TDD. Notice how you’re forced to pay attention to your interface before your implementation.
- Don’t forget Step 5 above. With TDD our initial focus is to get something working, but once we have that it’s just as important to make it clean.
We don’t just have examples of how the system functions, we have a living documentation system
We discussed how tests are held to the same standards as production code, meaning they should be kept just as clean. This has far reaching benefits beyond just resolving errors faster.
Documentation is expensive. We must constantly maintain it, otherwise, it rapidly loses its value. Yet interestingly, both tests and documentation ultimately do the same thing. They demonstrate what our application does. The only real difference is the audience.
Clean Code should read like “well-written prose”, according to expert Grady Booch. If our tests read like well-written prose, they themselves can serve as our documentation. Furthermore, assuming our tests are passing (and they fail for the right reasons) then they are always up to date with what they are documenting. Hence we have a self-maintaining i.e., living documentation system.
With a living documentation system, developers spend less time updating documentation and more time delivering value. Furthermore, management and even new developers joining the team have a reliable source of information on what the application does. It’s a win-win!
- Try writing your test names as sentences (a tenant of Behavior-Driven Development). State what the behavior is your test is demonstrating e.g., “Stack should overflow if capacity is exceeded”. Using a modern testing framework can help here.
- Once again, keep your tests clean. Using appropriate naming and keeping them small go a long way towards making them readable.
We hope you’ve enjoyed learning how Clean Code leads to rapid, maintainable, and predictable software engineering through improvements to naming, functions, and testing.
There’s so much more to learn, and it can be hard to find the time. That’s why it’s important to have a support network around you to foster Clean Code practices and lifelong learning. Check in for our final entry next week to see how at BTI360 we achieve just that!
Read the Other Parts of the Clean Code Series:
- Part 1: The Difference Between Programmers and Software Engineers
- Part 2: Why Clean Code is Important for Developers and Management
- Part 3: 4 Clean Code Naming Principles Every Developer Needs to Know
- Part 4: What Every Software Engineer Ought to Know about Functions
- Part 5: Does Testing Really Make You Go Faster?
- Part 6: Teamwork Makes Clean Code Work