When you dive into automated testing and test-driven development, it won’t take long before you learn about the testing pyramid. If you’ve never heard of it, don’t worry. I’ll explain what it is first, then go into some variants, and finally explain how you should use it as a guide toward a quality test suite.
What Is the Testing Pyramid?
Mike Cohn first came up with the concept of the testing pyramid in his book Succeeding With Agile. He puts automated tests into three categories:
- Unit tests
- Service tests (more often called integration tests)
- UI tests (or end-to-end tests in these days of APIs)
The idea is that you should have many unit tests, fewer integration tests, and even fewer UI tests. This picture does a better job of explaining it:
As you can see, the unit tests form the large base of the pyramid. Above that are the integration tests, and at the top, you have your end-to-end tests. The higher up the pyramid you go, the less of these types of tests you should have. Why? Because the lower tests are smaller and run faster. And their smaller size makes them easier to maintain, read, or change. So you want to have more of those tests and fewer tests that use a lot of moving parts. End-to-end tests are notorious for being harder to read, slower to run, and easier to break.
Since the testing pyramid was coined in 2009, it has guided many teams toward good test suites. But one issue that it can lead to is silly discussions about differences between unit and integration tests. When does something stop being a unit test and become an integration test? Is it when it uses two or more classes? Or when it crosses module boundaries? These arguments never really lead anywhere, which is why the modern interpretation of the testing pyramid removes the distinct boundaries.
Ham Vocke made a diagram that teaches us to have fewer tests that integrate more and run slower. Conversely, you should have more of the faster and more isolated tests. In my opinion, Seb Rose brings across the point even better with this diagram:
Once the idea of a testing pyramid was established, several variants were identified. These variants are anti-patterns that you should avoid.
The Ice Cream Cone
This is an inverted pyramid and happens when you have too much manual testing, more end-to-end tests than integration tests, and not enough unit tests. The automated tests that you have are using too many pieces of the application and run too slowly.
This often happens in legacy codebases where finding ways to add unit tests is hard because of all the highly coupled components. It also happens when the development team lacks the necessary skill or discipline to write code that’s loosely coupled and uses abstractions.
This happens when the team writes a decent amount of unit tests but then uses end-to-end tests to test the integration of all the components. Integration tests (i.e. tests that use fewer components) could be sufficient for many of these scenarios and would be more maintainable and run faster.
The testing cupcake can occur when different teams are isolated from each other and/or don’t cooperate. The developers are writing unit tests and integration tests, a UI team is writing automated UI tests, and the end user or a testing team is performing manual test scenarios. Tests overlap in the functionality and scenarios that they are testing, nobody is really talking to each other, and it’s generally a big mess. When developers change something and change their tests, they break a bunch of other tests that they have no control over.
This pattern derives its name from the fact that the testing pyramid sometimes contains a cloud at the top mentioning manual tests. When you keep this cloud but have a mass of different automated tests beneath it, you get a cupcake.
So now that you know what the testing pyramid is, how should you use it to guide you? As the title suggests, the testing pyramid is a blueprint for automated testing. So we’re going to leave manual testing out of the equation for now.
When you start developing a new feature, you should break up said feature into smaller chunks. You can then write unit tests for these chunks. Keeping the focus small allows you to
- receive very fast feedback on your work
- continuously check if your changes haven’t broken anything
- think about edge cases for the piece of code you’re writing
- write your code in a decoupled way
After a while, you will have to connect all the pieces. This is where integration tests come into the picture. With integration tests, you can have a test use the different building blocks you wrote earlier without having to worry about testing all the special cases.
Finally, you can zoom out and write a limited number of end-to-end tests, or maybe even just one, depending on your feature. This is where you would introduce pieces that are more external to your system, like databases, file systems, and maybe even external applications.
If you follow this pattern, you’ll automatically end up with a testing pyramid.
In case you already have an existing application with an extensive test suite, it might be interesting to evaluate what kind of pattern you have. If it’s one of the anti-patterns, you might want to try and deconstruct certain tests to move them downwards, or get together with other teams, in the case of the cupcake. Sometimes, test suites consist exclusively of unit tests. In that case, you could easily start adding some integration or end-to-end tests to build up your pyramid.
If you have absolutely no tests, don’t despair. You can start writing your test suite with the testing pyramid in mind. Just make sure you avoid the previously mentioned anti-patterns. In a codebase without tests, it’s especially important to avoid the ice cream cone, as writing end-to-end tests will initially feel easier.
Build Your Pyramid
The testing pyramid is not a new concept. It’s been applied and approved by many professional software developers. It’s proven to be a good guideline toward a test suite that provides value and is a joy to work with. Remember that the pyramid has been updated to remove the explicit differences between types of tests. But the idea remains the same: you should have fewer tests that use a lot of pieces of your application and more that run isolated pieces of code.