Hi, I’m David, and In this article, we’ll be exploring together how we can make sure that our code doesn’t… mess everything thing up. And along the way we’ll see that the solution to this problem has added benefits, including one you probably hadn’t thought of.
But first, why do we have this problem? Why is our code fragile?
First, keeping track of all the information we need is impossible. The needs, requirements, and technology… evolve. The systems we deal with have complex interactions. It’s impossible for your brain to hold all the rules and behaviours that are supposed to happen. All the specifications. All the use cases. Everything the code is supposed to do.
How can we solve that?
Before we go into that… the situation is (in fact) even worse than it seems.
Because, as developers, some of the systems we work on deal with vital resources. We could be making sure people get paid on time or receive their Christmas gifts… or managing inventory that means life or death for patients in hospitals, or coding self-driving cars.
Obviously, (and thankfully) not everything we do has that much importance. But some of it does. And even if all we are doing is making someone’s life easier, with a tool, or more fun, with a game… Well, that’s important too. People depend on our code.
How can we ensure our code doesn’t mess everything up at the wrong time?
Let’s be frank: it’s not humanly possible. There is too much information to hold in our brains. It’s too complex; it’s beyond our capacity. But thankfully, I have good news.
All this is within a computer’s capacity!
Here is what we can do: write additional code and have that code test our code. And that, ladies and gentlemen, is what automated testing is all about. It’s about writing more code to test that your code is doing what it should automatically.
And what are the different types of tests? And what are the added benefits? But first: How does that work in practice?
Well, basically, we take our code — a function, for example — and say : when I send this input to the function, it should be returning this. The basic canonical example is, if I call the
add function with 2 and 3, it should return 5. That basic concept can be extended up all the way to interactions with the interface. For example, on a website : if I click on the button on the website, and I enter a login and password page that are valid, then check that I’m redirected to the next page.
The simple, atomic testing of function that only have internal logic, that don’t depend on the state of other parts of the code (or to use the technical term, “pure” functions), is called unit testing.
Testing the whole system together like my example with the login screen is called “end-to-end” or “UI” testing. And there is an intermediate level that is integration testing, where we are not interacting with the full user interface, but still making sure bit work together.
This could typically be calling an API or a complex function that uses other parts of the code.
And my preferred way of testing is actually that intermediate way: integration testing. Why?
First, end-to-end testing tends to depend on how the user interface is implemented. And if there is one thing that stayed the same across the many, many years I’ve been developing, both in games, and app, and websites, it is that user interfaces change a lot. So end-to-end testing is a pain.
Conversely, unit testing means there is a lot of code to write, for every single little function.
Integration testing hits that sweet middle spot. It allows us to test a lot more of the codebase with one call.
(Oh, and Incidentally, the proportion of the codebase that is covered by automated tests is called “coverage”, you can see badges showing coverage on GitHub repositories).
What are the four benefits of automatically testing your code?
The most obvious advantage of setting up automatic testing of your code is that it provides better stability. Your code is less likely to break. Or rather, when it does break, you’ll know about it straight away. If you’ve set up Continuous Integration and Deployment, or CI/CD, you can add the tests to the deployment to make sure you only push clean code out.
But stability is not the only advantage.
You see, we often need to refactor our code. To write the code in a way that is cleaner. Or a way that takes new information or technology into account. When we do that, we want to make sure that the code has the same behaviour before and after being refactored.
And there is no better way to do so than to have automated testing set up. It’s a kind of contract that says : this is what we expect as output when we produce this input. Because when we’re refactoring, we basically want better written code that does the same thing as it did before. And making sure the code behaviour does not change is precisely what automated testing is best for.
But that’s not the only advantage.
Better architecture & debugging
You see, when we implement testing, it forces us to structure our code differently.
Imagine you want to test. Imagine you have a function that does loads of stuff like calls an API then does a load of stuff with the data and saves it, and it is not working. I say “imagine” but that is precisely what happened to me last week. I was adapting a code snippet I’d gotten from an AWS blog article. And part of it was not working.
And writing tests allowed me to quickly understand where the bug was coming from. But writing the tests, and structuring my code so that it could be tested, make me write better code because I ended up better isolating the different bits of the long function into smaller more manageable bits.
Now, before I go into the fourth advantage, we need to talk about something else:
What are the costs & downsides & pitfalls of automated testing?
The disadvantages of automated testing. Because everything isn’t perfect.
For example, you can also fall into the trap of writing too many tests for things that don’t deserve it. I mean, in real-life testing the add function is just plain stupid : we know the plus operator works, and if it doesn’t… then we have other problems.
Conversely, if your tests don’t cover the error cases, they can lure you into a false sense of security. You can end up thinking things work because a test passes, but if you’re not really testing all the cases, your code might only work partially.
Finally, at times, it feels like writing tests is just as long and complicated as writing the code itself.
But even that additional cost is worth it when you see the fourth and final benefit.
Peace of mind
You see, sometimes people depend on your code working well. And that can be stressful. Automated testing gives you the peace of mind that your code is behaving as it should. That messages are being sent. That payments are getting delivered. And that security, that peace of mind, can be vital to our mental health, as developers. It removes stress.
And although at times it might feel like automated tests add development time.
But writing tests is actually faster in the long term. Because well-written tests can work as a documentation and a safeguard. They’re an investment against technical debt. They help discover and solve bugs much faster.
Automated tests are an indispensable tool to have in your tool box. Both for the quality of your code, and your peace of mind.