Software tests can serve a number of functions
-
Verify correctness - This is the most common purpose of tests. Desired functionality is defined and then codified into an automated unit test. The test tells whether or not the software artifact provides the desired functionality. This is useful when first creating the software, when adding new features, and when deploying to a target environment as a sort of sanity check.
-
Catch regressions - This is related to the above. When refactoring a certain component, it is necessary to have a battery of tests that will determine whether or not the behavior of the component has changed. In theory, refactoring software means changing the internal structure and operation without changing the user-visible behavior overall. That is, the contract is still upheld, even though the implementation details change. Unit tests serve to ensure that said contract is being upheld. If the tests are comprehensive of the desired behavior and they pass, then you know the refactorings won't break users of the artifact. If the tests fail, then the refactorings are not acceptable.
-
Demonstrate usage - Unit tests can serve as a kind of documentation. Unit tests for a particular method, for example, show the ways to call that method. By using a large number of different sets of input parameters, together with the expected outputs, the tests can demonstrate the problem space and the resulting conceptual model of the unit under test. Of course, this requires that the names of various test methods are sufficiently descriptive, and comments provide clarification where needed.
-
Force the developer to think about architecture and usability - This one is the least tangible. It’s also probably the most difficult to see the benefit of at first glance. When you’re writing tests, and you come upon a component that is difficult to test (e.g. lots of internal state that’s difficult to set up), you quickly realize that maybe the component is not as usable and flexible as it needs to be. This then spurs you to change the component to make it more testable, e.g. by making the internal state easier to set, or by using dependency injection.
-
Reverse engineering – If you have an old program or library that your company wrote a while ago, the source code of which has since been lost, you may have no idea how it works. The motivation for this could be anything from adding new functionality, fixing a bug, porting to a different platform, or any other goal of software development. The critical problem is the lack of source code. Making any change to the existing program is impossible. Assuming you’ve exhausted all other options, you will have to re-write the source code completely.
Fortunately, however, you’re not doing it from scratch. You still have the old, compiled version of the software that you can still execute. In such a situation, you can use exploratory testing with the compiled software to figure out what it does and how. Once you reliably determine some aspect of its behavior, codify it in a unit test. Then, when you start writing the new source code, simply run the exact same unit tests to see if the code you’re writing matches the behavior of the previous version.