Maksim Kabakou - Fotolia
Unit testing is a powerful tool for software quality -- and has been for decades. Unit tests provide a fundamental check that an application meets its software design specifications and behaves as intended.
When done well, unit tests:
- decrease defects and expose them early in the development lifecycle;
- increase code readability;
- enable code reuse; and
- improve deployment velocity.
Unit tests, a type of functional test, have reached majority adoption; they are simply how many development teams do business. Yet, the companies I advise tend to use unit tests sporadically, and with low coverage. While the development environment supports unit tests and most of the programmers know the basic frameworks and tools, they only use unit tests for easy and low-value instances.
Let's explore why unit testing is important, the origin of this type of testing and some of the barriers to adoption.
The history of unit testing
A bug caught early is time and effort saved. For the first 50 years of computer history, unit testing and debugging were essentially the same thing. But, by the 1990s, code had become so complex that it was often impossible to break systems into small pieces and run them in isolation.
In 1997, a programmer named Kent Beck created JUnit, a development environment plugin to test small pieces of code. Developers wrote test code that assessed the source code. He called the approach unit tests. This style of unit test tool became a staple of every major development environment for years.
After Beck created JUnit, Martin Fowler wrote the book Refactoring, which suggests ways to transform code to make it more isolated and testable. The combination of code refactoring and unit testing led to test-driven development, where unit test creation is essential to the development process. In TDD, code must be testable before it is even created. What the code does, and its exception conditions, are defined and testable.
Thus, we have the adage: The programming isn't done until the unit tests can run (pass). From there, the project can move on to system- or human-level exploration.
Unit testing example
This example demonstrates the importance of unit testing. Here, JUnit evaluates a simple function that converts temperatures from Fahrenheit to Celsius. The formula for the conversion is: C = (F-32)*5/9. It's trivial -- only a few lines including the function signature and curly braces -- to implement in code as a library function. What is not clear from the function, however, is criteria. These options could include whether the values round up or round down, are real numbers, or have upper and lower limits.
Let's create example unit tests for this temperature conversion function in Perl, using a module called Test::More. The first line is a comment that tells the programmer what to expect from the remaining code.
# is (input, expected result, comment) is( FtoC(32),0,'Freezing point is F32, C 0'); is( FtoC(212),100,'Boiling point is F212, C 100'); is( FtoC(59038),32767, 'Upper limit of C is 32767'); is( FtoC(59039),undefined, 'One past upper limit is error');
The JUnit framework relies on object-oriented systems and testing objects, but the concept is similar.
Unit tests in isolation
One of the benefits of unit tests is that they isolate a function, class or method and only test that piece of code. Higher quality individual components create overall system resiliency. Thus, the result is reliable code.
Unit tests also change the nature of the debugging process. To attempt a bug fix, programmers simply write a failing test, then iterate to make it pass without breaking a previous expectation. This process eliminates the manual loop of traditional debugging through set up, re-create, pause and inspect.
Changing code to make it unit testable requires programmers to change how they work. Any code snippets written without unit tests are likely to be considered untestable, at least as individual units. Programmers that don't have to unit test tend to be change- and risk-averse.
Unit test adoption
Legacy software refers to software that has been running for a long time -- and often written without unit tests. Legacy code has value to the company. Some programs built without the benefit of unit tests process a million dollars' worth of profit-generating transactions a day. But code that doesn't have unit tests typically is a big ball of mud that evolved over years with many different maintenance programmers touching it.
Without unit tests for guidance, programmers get a change request and try to make as small a change as possible and get out; they don't want to cause the mess to topple over. Ken Schwaber, co-creator of the Scrum framework, calls this situation the core system problem.
Refactoring enables programmers to, piece by piece, make system changes to make it testable. These changes, however, take time. Programmers who adopt a unit test framework will build code more slowly than ones who don't. The programmers who don't do unit testing still make changes. But those might include a change to "step on" a change, and they might preset the expectation that the test will fail, also called turning red. When the unit-testing programmers update their code and see these failures, they will be the ones to fix the code, or green, the tests. That means that the programmers doing unit tests spend a great deal of their time in test maintenance, which slows them down.
A few years ago, I debated with my colleague Bob Reselman about unit test adoption for legacy apps. Reselman argued it was too expensive to inject unit testing into apps built without them -- even a fool's errand. Instead, he recommended an organization start out new development with unit tests, and leave the legacy applications alone. While this might be true for COBOL, report program generators and other applications, I argued that apps written in modern languages with familiar conventions -- so-called curly bracket languages like C++, C#, Java and Ruby -- can retroactively incorporate unit tests rather easily. Rather than write them for the entire application, just add unit tests to the current change, and refactor as you go. Over time, this process looks something like a strangler fig pattern: Good, unit-tested code over a legacy system. The entire team, however, must agree and work together on this. Either everyone works with the unit tests, or, in the long run, no one does.
Improve speed, quality and testability
Project managers say scheduling involves tradeoffs between quality, the amount of work done, resources and time. To add to one area, you must to subtract from another.
Effective unit tests break this rule. This is why unit testing is important and valuable for organizations. Good unit tests create testable code, which improves quality. That code will have fewer defects, which means fewer bug fixes, for faster project completion. When software bugs do occur, unit tests lead to faster debugging, fixing and writing code, and this is done in such a way that the defect is much less likely to repeat -- improving code quality and velocity simultaneously. While there are no silver bullets in software development, effective unit tests can speed up development, testing and even parts of functional requirements development.
Like other initiatives, time and attention are the main barriers to unit test adoption. Too many people have a fixed mindset, where new ideas are scary and risky. One example is what I call the name-it, claim-it mindset. In this scenario, once someone mentions unit testing, management assumes the technical people write unit tests, but don't confirm that it's getting done. Management often lacks the technical skills or initiative to check. Real success will come when groups invest a little time to get meaningful and important unit testing results.