Enhancing Software Quality Through Unit Testing
Unit testing is critical to software quality, especially in agile development. AI tools can help overcome the challenges of unit testing legacy systems.
TABLE OF CONTENTS
Defining Unit Testing
One of my first disagreements with my QA team at Ultimate Software was about the definition of unit testing. Many QA team members used the term when testing a small system section. In most cases, they meant story or feature testing. They executed the tests to verify that a specific story or feature was working as expected.
Current Definitions of Unit Testing
I tried to find out where the source of this confusion comes from. I checked some of the most famous QA books, and the definitions are similar. For example, Boris Beizer, in his “Software Testing Techniques” book, defines a unit as the smallest piece of software that can be independently tested (i.e., compiled or assembled, loaded, and tested). Usually the work of one programmer consisting of a few hundred lines of source code.
Today, no one will consider a few hundred lines of code a single unit. That is too big. Beizer was specific enough to indicate that unit testing is the work of programmers, for the most part. What kind of testing did my QA team do? Well, Louise Tamres does an excellent job of breaking down the levels in her book “Introducing Software Testing.” It is a question of scope. Here is her breakdown and definitions:
- Unit level testing concerns the testing of the smallest compilation unit of some application. The goal of unit testing is to demonstrate that each individual unit functions as intended prior to integrating it with other units. The tester typically invokes the unit and enters data via a test driver that sends the test execution information to the unit-under-test.
- Integration level testing consists of combining the individual units and ensuring that they function together correctly.
- System level testing applies to the entire application as a whole. This is often how the end user views of the system. The tester typically interacts with the application by entering data the same way as would the end user.
Tamres' definition assumes you cannot access a unit through the user interface (UI). My QA team did all their “unit testing” through the UI. They were testing at the system level, no matter what they called it.
I always wonder if we have three levels of testing. The line between integration and system testing is blurry, but that is a subject for another blog post.
Agile and Unit Testing
Our understanding of unit testing underwent a significant transformation with the emergence of agile software development. Test-driven development (TDD), a practice within extreme programming (XP), revolutionized the concept of writing small isolated tests to verify functions or methods of the code. Writing the unit tests before writing the actual code was a game-changer, inspiring a new approach to software development. The “Red-Green-Refactor” TDD cycle, which involves writing a failing test (Red), writing the minimum code to pass the test (Green), and then refactoring the code to improve its design (Refactor), changed how software engineers think about writing code and set a new standard in the industry. The following diagram illustrates the process:
TDD became the way to implement Quadrant 1 of Brian Marick’s testing matrix.
TDD has become the standard practice for implementing a technology-facing supporting programming suite of unit tests. Agile also brought an explosion of unit testing frameworks. Kent Beck, one of the creators of XP, created one for Java called JUnit. Today, the most popular programming languages have at least one unit testing framework to support implementing TDD.
The publication of the agile testing pyramid established the importance of unit testing within agile software development.
The agile testing pyramid calls for most testing to happen at the unit level. Other models have challenged this recommendation. For example, the trophy model recommends more integration tests than unit tests:
Still, unit testing has become an essential part of software development projects. Agile engineering practices like TDD, BDD, and CI/CD are less common in software development. One reason for this dichotomy is the existence of legacy software. Implementing these practices can be challenging, especially in legacy systems. It often requires significant refactoring and a shift in mindset. Understanding these challenges can help you prepare for the realities of modern software development.
Unit Testing Challenges
Legacy Software and Unit Testing
Like most development teams in the early days of agile software development, we struggled with implementing agile practices while supporting multiple legacy systems. We had no unit tests, and adding them to an existing system was challenging. Michael Feathers addressed that experience in his book Working Effectively with Legacy Code. This book is one of the most important software engineering books. Every software engineer should read it, as it provides strategies and patterns to deal with legacy software and how to refactor it to add tests, offering support and solutions to everyday struggles in software development.
Michael Feathers defines legacy software as code without tests. He provides strategies and patterns to deal with legacy software and how to refactor it to add tests. As expected, unit tests play an essential role in Feathers’ book. He defines what is not a unit test as:
- It talks to the database
- It communicates across the network
- It touches the file system
- It can’t run at the same time as any of your other unit tests
- You have to do special things to your environment (such as editing config files) to run it
While not on this specific list, a unit test cannot be slow (run in less than 100 ms). In other words, it needs to run fast and be completely isolated from the infrastructure components and other unit tests. Michael Feathers discusses seams, characterization tests, and different techniques and patterns. We will not cover them here; we highly recommend reading the book.
This idea of having very fast isolated tests also brought attention to a set of supporting tools and techniques. The popularity of agile software development and agile practices like TDD enable the popularization of mocking framework tools.
Unit Testing Today
The history of unit testing follows the history of software. While we can find references to unit testing as early as the 1950s, it was only with the arrival of agile software development that unit testing became a must-have practice. Today, we talk about unit testing in the context of TDD. All significant IDEs today support unit testing and practices like TDD. However, TDD and unit testing are different. For most of the history of software development, we wrote unit tests after writing the code, not before.
Even today, only a small percentage of software developers follow TDD. According to a survey published in September 2020, only 8% of developers who responded write tests before writing the code. The good news is that according to most surveys, a significant percentage of software developers write unit tests (over 40%), even if only a few follow the TDD process.
Unit testing is an essential part of a comprehensive software testing strategy. It is how we implement quadrant 1 in Marick’s test matrix. But we need more teams to practice it. Can AI help? We think so.
AI and Unit Testing
One area in which AI can have a significant impact is unit testing. For engineers without experience writing unit tests, many AI tools can help them. The following is a list of tools that already exist and how they can help you write unit tests:
- DiffBlue Cover is an AI generation tool for unit testing on Java codebases. It uses reinforcement learning to generate and optimize tests.
- GitHub Copilot is Microsoft’s tool. It is powered by generative AI using OpenAI and trained on all languages appearing in public repositories. It works like an AI pair programmer, providing code suggestions.
- Tabnine is an AI coding assistant. It supports the most popular IDEs. They use a proprietary model. It supports code generation.
- Google Cloud’s Duet provides code completion and generation for developers. It leverages Google AI foundation models.
- Amazon Codewhisperer is an AI-powered coding companion from AWS. It proposes code snippets to full functions across 15 programming languages.
Based on these examples, you will notice the trend towards code assistants. In that approach, you get suggestions as you write code or ask for recommendations within your IDE. Still, several tools support unit test generation by scanning the codebase.
Final Thoughts About Unit Testing
To achieve a comprehensive testing strategy, you need to implement unit testing. It is the best way to cover quadrant 1 of Marick’s test matrix and provides the best set of tests for quick feedback. You do not have to implement TDD to have unit testing, and the emergence of AI code assistant tools makes it much more manageable.
About the Author
Testaify founder and COO Rafael E. Santos is a Stevie Award winner whose decades-long career includes strategic technology and product leadership roles. Rafael's goal for Testaify is to deliver comprehensive testing through Testaify's AI-first platform, which will change testing forever. Before Testaify, Rafael held executive positions at organizations like Ultimate Software and Trimble eBuilder.
Take the Next Step
Join the waitlist to be among the first to know when you can bring Testaify into your testing process.