Unit Testing 101 For Non-Programmers
Seeing the many challenges in the field in terms of adopting unit tests, I am totally convinced that a basic awareness of the inner workings of unit testing needs to exist even for the non-programmer who works closely with software delivery teams to ensure that teams use unit testing to help produce high quality software that is easy to enhance or modify. This article explains the fundamentals of automated unit testing as well as how to adopt automated unit testing within your organization.
What is Unit Testing?
The goal of unit testing is to take a small piece of code that is responsible for enabling some very specific functionality within the software being developed, and to test it to ensure that it behaves exactly like you would want it to under various conditions. This approach allows us to test internal parts of software that are not typically exposed directly to the end user, for example, through a graphical user interface /screen, and therefore cannot be validated easily through conventional testing. This form of testing also provides early feedback for the programming teams as to whether they are headed in the right direction and allows them to make small alterations if necessary.
“Those who are enamored of practice without theory are like a pilot who goes into a ship without rudder or compass and never has any certainty where he is going.” Leonardo Da Vinci
In conventional testing, which can be either manual or automated, validation of functionality typically occurs after the software is developed by which time it becomes nearly impossible to resolve any critical defects or design flaws quickly, and almost always delays the delivery of software. However, with unit testing, the programmer’s work is validated much more quickly by testing small modules of the software as they are developed, allowing for quick changes to be made if defects, or deviations from the original design are detected.
Unit testing has been around for a long time, but tools to support unit testing have gained popularity only since the advent of JUnit (written by Kent Beck and Erich Gamma). Although Kent did write another Smalltalk-based unit testing tool called SUnit many years previously, it was only with the increased popularity of the Java programming language (and the industry’s quest to develop high quality enterprise-wide applications with it) since the late 90s that unit testing came to be increasingly adopted within the industry.
There are many challenges to the adoption of unit testing. These range from the fear of doing anything new, lack of desire to increase code quality and productivity by programmers, and, a lack of awareness of good practices around unit testing as improperly implemented unit tests can sometimes do more harm than good. These concerns can be addressed effectively by reading and understanding what others have done, followed by small experimentation before proceeding towards full, or at least limited adoption.
My own experience with building new software as well as in maintaining legacy software over the last 12 years has led to me to believe that the advantages of unit testing are often not plainly visible to programmers and customers alike, and that unit testing is often relegated to lowest priority especially when development timelines are tight. Sadly though, this is precisely when unit tests prove to be most beneficial as it cuts down on the stress levels of the entire team by permitting them to make even drastic changes without the fear of breaking existing or working functionality.
Art of Reasoning, and Why Unit Testing (and Software Development) is Totally Awesome…
Although the stereotypical image of the software programmer often portrayed in the movies is that of someone who is extremely introverted, and morbidly attached to his/her computer, all good programmers I know are friendly individuals, and channel their creative abilities and curious natures on many hobbies, including “extreme” sports. However, more than anything else, the aspect of their lives they enjoy the most is always the challenge of applying their reasoning abilities to solve what many would consider as fundamentally logic-related problems. When developing software, many programmers apply reasoning, and build complex software using what I would call as “axiomatic” principles. That is, you build something small, verify that it works, and later, you build progressively bigger things on top of the previously validated “building blocks”. By doing it this way, in small incremental steps, you can be sure that the logical foundations of the software are always correct.
This is where unit testing comes in and provides maximum benefit for the software programmer. By taking small portions of code, and by verifying that they work through incremental testing, we can later assemble the pieces of “validated” code blocks together into larger units, and at the same time be assured that software will do what we expect it to do. This approach also provides many opportunities for the programmer to clarify on features that are requested by the customer as it is nearly impossible to anticipate every possible scenario in which the software is intended to be utilized. This helps minimize the problem frequently seen in the industry, which is “paralysis through analysis”, an unfortunate situation in which teams spend way more time in analysis and design phase than necessary due to the fear that they have not captured every possible scenario and are unable to proceed towards software construction quickly. When unit tests are used in conjunction with another practice called Continuous Integration (which I will touch on in the next section), they can help communicate the true progress of any software development-related activities to non-programmers such as project managers and executives as opposed to simply using status reports which can be quite deceiving/misleading at times.
If you are interested in getting a high-quality software application developed with the help of unit testing, you need to understand some vocabulary that experienced programmers frequently use when dealing with this aspect of software development. Gaining some understanding of this area will also help improve the communication between you and the programming team.
First, there is “System Under Test” (often abbreviated to “SUT”) which refers to whatever small module or piece of code that we are interested in unit testing. And there are other terms including “Class Under Test”, “Object Under Test”, “Method Under Test” and “Application Under Test” which are abbreviated to “CUT”, “OUT”, “MUT” and “AUT” respectively. The first three acronyms refer to the small bits of code, or modules, which the programmer often uses like building blocks, and which are responsible for enabling discrete pieces of functionality. “AUT” refers to the entire software application that is being developed. There is also one more important acronym “DOC” which is “depended-on component”. This refers to parts of the code that are not being unit tested, but still are required to be run to perform the unit test against the system under test (SUT).
Another term that you will need to understand is a “test fixture”. This is essentially simulated data to help create the pre-conditions needed for the test to work successfully. “Continuous integration” is another term you will often hear in the realm of unit testing, and is an advanced practice utilized amongst teams comprising of many programmers working on different parts of the software simultaneously. This process enables the team to automatically build or assemble the software often to whatever functional state that is realistically possible on any given day. All the unit tests that accompany the code that the various team members have committed into a central repository are also automatically run during this time, and this entire “build” of the software is considered a success or failure based on the results of the unit tests. This approach allows project teams to track whether the small modules that programmers are building can interact with one another successfully and provides frequent feedback for team leads to ensure that the team is progressing slowly but steadily towards the overall design specification.
“Refactoring” is yet another term you will hear and is the practice of changing the structure of existing code without changing its behavior. This practice is often used in combination with unit testing and is used to improve the design of existing code while attempting to add more functionality. Think of this as a clean-up of current software code to make the task of adding more code easier as code can become hard to read or maintain over time. With a safety net of the unit tests, the programmers feel much more confident about changing the any existing code without worrying whether they broke something in the process. There is also another term “test-driven development” often shortened to “TDD” which is a habit of building one test at a time, and later writing the real code to accomplish the behavior that the unit test expects to ascertain. This approach allows the programmer to only build what is absolutely required.
“Code is the serialized version of a mental machine.” ~ Patricia Aas
Adopting Unit Testing
If your team does not do unit testing currently, then ask them to try a small pilot preferably on something that is being newly developed. Retro-fitting existing code to be unit testable requires a little bit more experience as well as patience on the part of programmers and customers before they start realizing the many benefits that this practice brings. Bringing in an expert even for a short period of time should help in these situations. Typically, unit testing business logic-related code is far easier than testing user interface-related code or code that is responsible for persisting data into the database as there are far more variables to control during the test. Most often, the programmers who are best suited to be in on successful pilot implementations are curious and eager to learn and want to improve their productivity and that of their teams. Be supportive of these people as they are bound to struggle with other more cynical programmers who are not likely to try anything new easily. However, some level of cynicism is still good as you are better off adopting this practice in a slow but steady manner. This way, your team can collectively gain proficiency around unit testing, and work more cohesively together.
This is all I can cover in an introductory article and have taken some liberties at simplifying many concepts for easier understanding. I hope you still found it useful for understanding this practice which is essential for delivering quality software. Automated unit testing can increase the speed at which defects are fixed, as well as allow new features to be added quickly. Teams that follow unit testing also tend to have better morale as there are fewer opportunities for “finger pointing” as most defects are caught before any code changes made by individual programmers are shared with the rest of the team. This enables these teams to work more productively, and without friction, and produce great software for you. Unit testing also allows for other programmers to join or replace the original developers if they leave and become productive very quickly. Please feel free to send me an email if you have any comments or criticisms regarding this article.
Update on Oct 29th, 2023: This article is dedicated to an old mentor, Greg Hutchinson, who taught me many things about how to approach software development nearly two decades ago, and who passed away recently! (Rest in Peace, Hutch!)