The Advantages of Performing Unit Testing – The Ladder of Testing and Maturity
The software testing landscape has significantly evolved over the past year. Shifting testing left and right, covering more types of testing within the famous test pyramid are becoming a common practice within organizations. With these advancements the questions that are often brought up when building software and the testing strategy for the product are – which testing types are critical? Should I prioritize integration and functional testing over unit and API testing? When in the process and upon which trigger should I run the tests? And many more.
To properly answer these questions, it is important to realize that within software testing – there is no one size fit all response. It depends in many cases on the maturity of the organization, the skillset within the team, the application types, the application complexity, the target customers, the technology stack on which the application is built, and more.
In this blog, I would try to explain that while there are many moving parts and differences across application types, software development processes, skills, and maturity, there are few practices that are common and do fit every team.
I will start with Unit Testing since it is in my mind an imperative for code quality from the early stages of development and has many benefits when done right.
Advantages of Unit Testing
In a recent blog on software testing introduction, I saw this nice definition from Raz Gaon.
Unit tests are the base layer of testing. The idea is to test the smallest testable components of the program, commonly referred to as "units". A unit is usually a function, and as we know, functions should usually have only one purpose. Unit tests aim to validate the proper behavior of the smallest components in isolation.
This is an accurate and solid definition of this testing type. I also think that Roy Osherove’s definition is spot on.
A unit test is an automated piece of code that invokes a unit of work in the system and then checks a single assumption about the behavior of that unit of work.
When done right unit testing serves the following objectives:
- Fast feedback and debugging of defects in isolated code areas – Write automated tests before being required to fix bugs.
- Higher quality of the product by covering core pieces of the application as it’s being developed
- Clean code for ongoing maintainability and reusability (think with the end/future in mind)
- Test governance – Instead of running large suite to cover dedicated application areas, run smaller pieces of tests focused on specific features.
- White box/Dev Friendly Testing – no need for advanced setups, tools, frameworks, developers write code within their development environment. Easy, cost-effective, productive!
- Consistent, Reliable, and Trustworthy – Unlike end-to-end and other testing types, unit tests are less likely to be flaky and inconsistent, so developers would always get the same results unless there is a real bug.
The Ladder of Coverage and Maturity
While all the above objectives and advantages of unit testing are clear, this type of testing is not the only testing type that is required to ensure high and continuous quality of our software.
In Martin Fowler test pyramid, there are 3 layers of testing. Unit is the basis of it and seems to consist of the larger piece of the pie. It also shows that the cost of writing such tests and the velocity of running and maintaining them is much quicker than the other testing types.
What is also stated in Martin’s blog is that there needs to be a balanced strategy and approach to software testing. Here, I would state that the balance should depend on several things, and this is where the ladder can come into play.
Performing continuous testing that include unit, APIs, functional, integration, exploratory, performance, and production monitoring should be built on stable foundation.
Such testing types depends on the following:
- Tools availability
- Software development lifecycle processes (Agile, TDD, BDD, waterfall, etc.)
- Time and velocity – also translate to organization maturity
- Application complexity and app being designed for automation.
With the above points in mind, to climb up and down the test pyramid continuously, organizations need certain level of maturity, skillset, and automation readiness. All the above mentioned testing types should be executed upon specific triggers: nightly, per code commit, etc., hence, having them maintained and automated and ready to use is key for success. Such readiness means – organizational maturity.
When gaining the ability as an organization to climb up and down this pyramid, teams can maximize their test and code coverage, exercise it more often, and end up with a higher application quality.
Dependencies for Unit Testing Success
Each of the testing types have certain dependencies, some technological like environmental access and platforms availability, and some are human -based like skillset.
Specifically, for unit testing success, the developers or whoever is creating these tests must have the proper testing dependencies ready within production but most likely as mocks.
Since unit tests are covering an isolated area of the code or functionality of the software, it needs to be able to communicate and utilize this isolated environment; be it a database, service of some kind, or other piece of code to be able to run properly. In a bit outdated but still relevant from CloudBees, they refer to dependency injection (DI) as a method to resolve some of unit testing dependencies challenge.
Another great read with example to both explicit and implicit dependencies that unit tests are relying upon for running and providing their unique value, it provides another layer of understanding of the complexity of building proper unit tests by taking into consideration the surrounding code base, its services and APIs, and environments (database e.g.).
As part of the unit testing dependencies, you should also differentiate between mutable and immutable dependencies because you might be depended on API services and other objects that are unchangeable and if you have a dependency on these that expects different outputs, it obviously won’t work. Mutable dependencies like databases can be shared across tests and be private depending on the type and nature of your tests.
As mentioned above, a way to avoid many dependencies when writing unit tests especially in the early stages of a project is by utilizing mocks. With mocks, the tests can be better controlled and be less dependent on software that is either still being built, constantly changes and unreliable, or that it’s too expensive to setup and tear down per each test execution.
With software quality and testing, it is always a matter of maturity, processes, skillset, and time that drives teams to be better and better. Practicing the different testing types and matching them to the process and skillset, as well as the project velocity is critical for the success of the entire team, and a great step towards continuous improvement.
Climbing up and down the ladder should become more and more easy as teams build more solid and reliable test cases that are automated, consistent, and cover the right areas of the application.
As in sport – practice brings perfection, so start climbing up and down the ladder :)
Writing a robust unit testing suite to find bugs & regressions early in the development cycle is time consuming and sometimes not possible. The good news? Snowmate can do it all for you, 100% automatically and effortlessly.
No AI, no magic tricks, pure deep tech.
Schedule a demo here.