I think we can all agree that testing code is valuable.
Well tested code assures the completion of acceptance criteria, guiding implementation by outlined requirements. It empowers you to extend and refactor with a sense of safety, knowing that existing functionality won’t break as a result of your changes. Over time, your team gains increasing returns as the growing number of tests reduce the likelihood of surprising, hard-to-diagnose bugs appearing and derailing production software, often coming at a cost of significantly more time than it took to write the tests in the first place. Not to mention, this increased quality lowers the chance of production bugs being exposed to your customers, potentially driving them away from your product as a result. These tests even act as a form of living documentation for your code, clearly describing intent…
The list goes on.
Ultimately, testing leads to increased software quality, reliability and customer retention, while at the same time reducing overall cost. Most developers are well aware of these benefits. Yet, many of the production software we write is severely under-tested.
Why do we seem to resist following a practice that is ultimately designed for the collective benefit of all stakeholders?
I believe it comes down to culture. A shared mindset across organizations to turn a blind eye to the undisputed value that well tested software brings. Perhaps it was pressure from management, instilled in the company from early contributors or developed slowly over time due to overall lack of diligence. No matter what the cause(s) may be, it’s ingrained in your company’s culture now and you’re stuck. This quote from Tom DeMarco and Tim Lister’s Peopleware captures the issue perfectly:
“People hate change… and that’s because people hate change… I want to be sure that you get my point. People really hate change. They really, really do.”
– Steve McMenamin, The Atlantic Systems Guild, 1996
People don’t like being pushed out of their comfort zones. Even if a change is clearly positive, it is in our nature to resist any irregularity that challenges our norm. It’s easy to sit back and coast along, but if you want to reap the benefits outlined above for your team, organization and ultimately the customer, change will have to be faced.
Breaking culture is hard, but it’s not impossible.
In the rest of this post, I’m going to dive into what I call habitual testing. A collection of techniques and patterns I’ve adopted across the organizations I’ve worked at that have helped slowly build testing awareness and intuition. This process aims to instil small bits of change over time, while gradually forming good testing practices as the team adjusts and observes the benefits.
I came across this tweet a while back from Guillermo Rauch that really spoke to me:
In only a few words, he was able to sum up much of my own philosophy of how to approach testing. Especially in teams that don’t support testing as a core part of their development process.
We only have so much time to test, so test smart_._
Some teams get turned off of testing large legacy projects due to the sheer amount of code that needs to be covered. Others might question the value of spending time writing thousands of lines of detailed test code to cover disproportionately less lines of production code.
I’m not advocating that you drop what you’re doing to test every line of code in your project. The point being conveyed here is that you should write tests that are valuable. 100% code coverage does not necessarily mean that your code is well tested. Not all code paths need to be unit tested with precision. Not all APIs need integration tests. Not every UI component/workflow needs end-to-end tests. It’s our responsibility as engineers to determine what tests will bring us the most return on the time invested in writing and maintaining them.
An often shared model illustrating different forms of testing is what’s known as the Test Pyramid:
This diagram is often used to emphasize the importance of having a wide base of small, fast unit tests that cover the whole codebase, with some integration tests to “fill the gaps” and a few E2E tests to tie everything together. Alternatively, if we were to break down the diagram by customer value (in terms of certainty in the product’s complete functionality) and cost (effort expended to write), we would get the following:
The more surface area a test covers, the more expensive (ie. time consuming) it is to implement, but the most value that it brings. Larger tests are less precise but more accurate at validating correctness by user story acceptance criteria. To put it another way, your tiny units of work can function absolutely perfectly but provide no certainty that the API your customers hit will respond in a way that is acceptable.
If we were to re-draw this diagram in a way that better represents this new mental model, we might get something like the following (as shared by Kent C. Dodds):
Here’s how this new diagram can be interpreted:
Apply these insights to your testing strategy to ensure you get the most return on your investment. Remember to take a step back before writing a test, reference the points above and ask yourself if your current approach is appropriate.
Another deterring aspects of fully testing a completely untested codebase is the potential need for heavy refactoring to support sane testability. Most code written without testing in mind is often untestable as a result (ie. giant functions, several dependencies, global state, etc). The older and larger the codebase is, the more daunting this task becomes. Combine this with an ever growing backlog of new feature work and looming deadlines and you’re left with a recipe for disaster.
I don’t blame you.
Tackling a large, untested codebase head first, much like jumping into an aggressive diet or workout routine, is simply too much to take on at once. The goal of having a fully tested codebase doesn’t seem feasible. What you want to do is to foster a habit of piecemeal testing, attacking the monolith one step at a time.
In her book Succeed, Heidi Grant Halvorson lays out a simple practice to overcome procrastination that she calls an if-then plan, in which we identify a scenario that we would like to respond to by completing a certain task.
“Planning creates a link between the situation or cue (the if_) and the behaviour that you should follow (the_ then_).”_
When the cue is encountered, the desired behaviour is triggered automatically. This is the key to the success of this plan, the sub-conscious response that we teach ourselves won’t tax our self-control and ensuring that the desired task gets completed.
By adhering to a few simple guidelines, you can teach yourself (and your team) the required if-then plans to leave every nook and cranny of your codebase with better coverage than when you first touched it, as if it were second nature.
Integrating these if-then plans into your development process introduces light testing sprinkled throughout your day-to-day tasks. Before you know it, you’ll be thinking about ways to test your code before you write it.
Charles Duhigg compares our willpower to a muscle in his book The Power of Habit, suggesting that there is only so much willpower we can exert before we run out of steam. Over-exerting ourselves early during trivial tasks results in depleted self-control and lack of discipline when the time comes to apply ourselves for the work that really matters.
Our work lives are full of tasks that require willpower. Checking emails, keeping JIRA tickets up to date and responding to Slack notifications gradually deplete your willpower reserves throughout the day. Eventually, you can become so drained that you start to slip and lack diligence when it comes time to work on more meaningful tasks (like testing your code).
While you can take measures to reduce the negative impact and frequency of these menial tasks, the reality for most of us is that we’re not in a position to block them out entirely. What we are in a position to do is make sure that testing doesn’t get negatively affected by our drained reserves. By making testing as straightforward and painless as possible, we reduce the effort required to write them and in turn minimize the risk of writing rushed, careless tests when we hit our lowest point of determination.
With the right tools at your team’s disposal, testing can become less of a chore and at times even enjoyable. We go to great lengths to make our development environment as pleasant to work with as possible. It’s time to apply the same mindset to our test tooling.
Sandi Metz gave a talk called Rules, which outlines 5 rules for writing object oriented code. Furthermore, she digs into how social scientists define rules and the impact they have on willingness and self-discipline. The results are illuminating, and the benefits can be applied across domains.
Ultimately, the rules you set are arbitrary and it’s perfectly okay to break them if reasonable. It’s the very existence of these rules that bias towards co-operation and collaboration, towards maintaining the “state of things”. By setting hard rules that govern how testing should be done, you are effectively influencing followers to uphold standards and push non-followers towards adhering to your norms.
When you start thinking about the kinds of rules you want to set for your own team, keep the following two metrics in mind:
Rules can have a profound effect on your team, wether they realize it or not. The metrics detailed above may not be perfect, but they serve the purpose of nudging team members toward the norm of constant, habitual testing.
Embracing effective software testing patterns can be a daunting, uncomfortable and downright painful experience. It doesn’t have to be. Take advantage of the mechanics hardwired in your brain. Create a framework that fosters the formation of habits that encourage a strong testing culture.
The final takeaway to making habitual testing a success is believing that it’s possible. Forming habits can be a challenge. It takes significant time and effort to realize the fruits of your labour, but the result is truly satisfying. To quote Charles Duhigg once again in The Power of Habit:
If you believe you can change — if you make it a habit — the change becomes real.
Develop positive testing habits that work for you. Don’t trust willpower alone to prevail. Leave conscious choice and debate out of the equation.
Make it automatic.
Thanks for taking the time to read this post!
Please clap 👏🏼 and follow if you liked what you read and join my newsletter to stay in the loop 😁
Originally published at www.igorbarsi.com.