In my previous post, I explained why you need automated tests. It’s cheaper, and more fail-safe. But not all tests are equal. And there is a place for each type of test. In this post, I will try to explain the basic types of automated tests in software development and lay out their use cases with advantages and disadvantages.
Most developers are introduced to automated tests through unit tests. The role of unit tests is to test a single unit of code.
What exactly is understood by a unit of code isn’t clear. But a unit test usually tries to use as little extra components as necessary. If the piece of code under test uses other components, they are often “mocked out” (also called stubbing or faking): replaced by a fake component whose behavior is controlled by the test.
Unit test should run very fast in order to provide quick feedback to the developer. They should also be small and readable so that other developers can quickly understand what’s going on. Finally, they should be deterministic: every run should produce the same output.
When developers write unit tests, they are often forced to use a good design and architecture of the code. Badly designed code is hard to test. Which is why automated tests should be written in conjunction with the actual code. It leads to better code and holds back technical debt.
In addition, it provides a way of being sure that no new bugs are introduced when someone changes the code. Just run the tests and you can be fairly sure that you didn’t break anything that worked previously.
A possible disadvantage of unit tests is that it requires some experience. Lesser experienced developers will work slower for a while and often find the process frustrating. Which is why they should be coached well by a more experienced developer.
Remember how I mentioned that unit tests try to avoid using other components? With integration tests, this isn’t necessary. Integration tests are all about testing the integration between different components.
In practice, developers can get into lengthy discussions about whether or not a test is a unit test or an integration tests. I don’t believe it actually matters, as long as a team can agree on the difference for that specific project.
End-to-end Tests or UI Tests
UI tests are a kind of end-to-end test. The idea is to test as many moving parts of the application. In case of an application with a user interface, we call them UI tests. But not all applications have a UI. In that case, they’re just called end-to-end tests.
It may seem attractive to ditch all tests in favor of these. After all, we want to test the entire application don’t we? Unfortunately, it’s not that simple. End-to-end tests tend to be slow and easy to break. Developers call them “brittle tests.” This means that the test can easily break, even though there isn’t actually an issue with the application.
The Test Pyramid
The test pyramid is a guideline for how many tests of these types you need. I’ve written about it extensively for NCrunch, a company that creates a testing product for .NET applications. The central idea can be shown in this graphic:
You want more of the “smaller” tests that don’t run many parts of your application. And less of the tests that require a lot of parts of your application.
The idea is that unit tests run faster and don’t break as easily. However, in legacy projects, this isn’t always so simple.
Testing Legacy Code
Code that wasn’t written with tests in mind is often hard to test. Especially unit test. Executing a single function may require all sorts of dependencies and cause all sorts of side effects. That is why it’s sometimes easier to write higher-level tests, like end-to-end tests.
Initially, this gives you a test ice cream cone:
Often called an anti-pattern in the past, this is not always the case anymore. Even UI tests can run very fast these days. And in the case of a legacy project, it’s better than having no tests.
But in the long run, you create a safety net to refactor the code so that it becomes easier to write unit tests. In the end, this gives you a testing hourglass. None of that really matters however. As long as your tests run fast, isolated from each other, reliably and give your team confidence to refactor. These automated tests then give them confidence to improve the quality of the code, without breaking things unknowingly.
Where To Start?
I outlined three types of tests here, but there are even more types of tests: property-based tests, contract-tests, regression tests,… Some of these definitions often overlap.
In a legacy project, I would probably start with higher-level tests. This gives me confidence to start changing the code. If I want unit tests, I often have to change the code before I can even write these tests. However, it depends on the specific pieces of code. If the legacy code does allow unit tests, I would just start with those. They’re easy to write and fast to run.