What are tests?
Mistakes happen, especially when we’re “in a groove” or trying to get things out fast. Taking the time to check what we have done, especially in development, ensures that we’re sending out a quality product. It can also save us some embarrassment.
Take the case of the Ariane 5 rocket, which took rocket scientists 10 years to build 🚀...and exploded 40 seconds after launch. 😱 During the launch, a conversion error between two software programs occurred. And BOOM! 💥 Can you imagine what it felt like to be that engineer?
A little extra testing would have saved $7 billion.
I get what testing is in theory... but what does it mean when we're working with code?
Testing is how developers check that their software behaves as expected and solves the right problems. You can think of it as a team sport, where developers and everyone involved in building an application pitch in to ensure quality. But what do tests look like?
Manual tests versus automated testing
For much of the short history of software, many teams used manual testers (people) who are responsible for the quality of an application by checking running software as they use it.
Sounds pretty good to me. Isn’t that enough?
Since people can only do one thing at a time and occasionally make mistakes, manual testing is often slow and unreliable. It's also not very practical since nowadays many teams release a new version of their software several times a day.
The alternative is creating custom programs which run and check code in many different ways. We call these automated tests. Since this is code, it’s fast and can be run over and over again in seconds! There are many different types of automated tests which you can choose between, each with their own pros and cons.
Types of automated tests
The three most common types of automated tests are unit,integration, and end-to-end, as represented in the testing pyramid below. The testing pyramid was first introduced in the early 2000s by Mike Cohn, one of the founders of the Agile movement.
As you can see, there is a higher volume of tests at the bottom of the pyramid, followed by progressively fewer as you climb to the top. The tests take longer to run as you go up the pyramid but provide more comprehensive, realistic coverage. Let’s check out the value of each one!
The biggest section at the base of the pyramid is unit tests that test small units of code. More specifically, they check that each individual class keeps its promises.
These are extremely quick, which means you can run a lot of them without having to sit around and wait for results. If you’ve broken something, you know about it fast and early. Since they only test individual units of code, they can’t tell you if everything in your app is working together correctly. And that’s where some of these other tests come in.
The next step up on the pyramid, integration tests, are slightly slower than unit tests, so you don’t write quite as many of them. Integration tests check if your units (classes) are working together as expected - assuming your unit tests are correct, of course! Since integration tests check interactions between units, you make fewer guesses about how well the final application will work. You sacrifice a little speed for realism.
End-to-end or system tests
These are the slowest tests but simulate reality more accurately. They eliminate any remaining risk by proving your application works with users in the real world (assuming your classes and their integrations are correct). For example, these tests could check whether users can switch your application on or achieve a goal. You only want a few of these tests here, as the higher up you go, the slower your tests.
Isn’t accuracy more important than speed?
The time it takes tests to run impacts their value and the cost of mistakes. Slower feedback from tests, like those from the top of the pyramid, has a real dollar value. It also means developers have to wait a lot longer to find out if they’re on the right track. That’s pretty frustrating!
For something to be checked with an end-to-end test, it has to be usable by a real user. It needs a UI or some other interface into the world. That’s where end-to-end tests run! If you were adding a new feature to your app and waited for this test, it would mean that you would have to write all the classes, integrate them with the existing ones in your app, and put a user interface in front of them. That’s a lot of work! 😅 If your test fails, some bad assumptions might have been turned into code. Fixing this will need a lot more time and work than fixing a mistake you find while unit testing!
The testing pyramid is a guideline that helps strike a balance between testing speed and code confidence. When you release software, you want to reduce the risk of introducing new bugs which break it and impact your users (or spaceships 😉). The lower this risk, the more confidence you have. By following the testing pyramid, you’ll be able to provide cleaner, faster, and overall well-tested code.
What should you test for?
Knowing about tests and tossing a few into your code isn’t enough to provide real confidence that your programs will do what they should. Let’s go back to the Ariane 5 rocket failure. Do you think they didn’t test? Of course they did! It was tested for years! But no one thought of testing that particular issue.
Test for the unexpected
Why do mistakes like that happen? When you think of a problem you want to solve with software, it’s easy to imagine a happy path. This is where the software is used just the way you’d expect, without any issues.
For example, a user might click through your app in the right order, where nothing goes wrong and ends up with a fantastic result. Or, in the case of Ariane, a rocket blasts off perfectly, or within some expected margin for error!
However, reality doesn’t always fit in with your plans. There are always going to be sad paths. When the Ariane rocket blew up, it was down to an unexpected bug which caused something bad to happen.
In fact, there will always be more sad paths than you can think of. Maybe a literature student accidentally pastes the collective works of Shakespeare into a form, or the network goes down.
But how do I make sure my application will work if I can’t think of every sad path or bug?
If you test for the more likely ones, you’ll reduce the risk of something unexpected happening, and you’ll get fewer bugs creeping in through your vents. To ensure that your software solves its target goals, your tests must check a mix of both happy and sad paths.
You can make sure they do by working with the people on your team who also care about the problem you’re solving to identify those happy and sad paths. Get together someone to think about testing, someone to think about engineering, and a product owner.
It doesn’t have to stop there though! If you still have doubts, bring in real users and other experts if you can. To make sure that everyone gets it, try and create some examples of how your application would behave in different situations. It’s easy to misunderstand small details when you work by yourself. So, talk to people!
From this, you write test cases together. Test cases just say what you would expect to see when something happens in your software. An example of this would be: “When pressing the convert button, the user should see the price in dollars.” You can then test your application by comparing what you expect with actual results.
Test to make maintenance easier
Even if you miss some important test cases, having a good amount already in place can make fixing and maintaining code a lot easier. For example, I once led a team which built a video player for a large news site. We tested our application really well, and eventually released it. Except that users soon started complaining that their mobile data was being used up when they hadn’t watched any videos. It turned out that our helpful player downloaded the videos even when they weren’t being watched. We hadn’t tested for this!
With the example above, we were able to quickly fix the video player as we had existing tests which would tell us if any of our new fixes had broken something in the existing code. This gave us the confidence to release a fix, knowing that our player would still work.
Test to communicate
There were a number of developers who worked together on this player, and they used their tests to help them communicate. Yes, tests can help you communicate!
When you’re in the middle of building software, you have a deep understanding of the code, what it’s supposed to do, and how you’d check it. You also think of new things to look for, like negative numbers or dates in the past. It’s a little like being engrossed in a book - you know the context and importance of every interaction in the story. But if you were to take a random page and give it to someone else, they’d have no idea what was going on. It’s the same thing with code. Imagine if you got transferred to a different city, and another developer had to finish writing your code. Where would they even start?
Well, your test cases will describe how people expect the software to behave. Reading your tests not only captures what your code is supposed to do but also how it works and how much of it works. If you write tests, another developer - or even ‘future you,’ if you pick up the project back up months later - will be able to understand what you were trying to do, and be confident about making changes to the code you were writing.
In other words, in a professional environment, testing is more than just a way to fix code - it’s a whole mindset. 🕶 In fact, in the next chapter, you’ll see how to integrate that mindset into how you code.
The test pyramid provides a pattern to help you write tests which give you confidence in your code as you make changes.
Test automation reduces the need for manual checks and when done right, can give you the confidence to fully automate the release of your software.
Unit tests check classes quickly and thoroughly to make sure they keep their promises.
Integration tests check that the classes and parts of your application which need to work together, do so by collaborating as expected.
End-to-end tests check that users would be able to solve their problems by using your app when it’s switched on. For a web application, you’d automate this with code which pretends to be the user clicking about within the user interface.
We’ll cover each of these in more detail within the coming parts. Now you're ready to get started with your first unit test in the next chapter!