After a detailed look at acceptance testing in software development, we now turn to the other end of the software testing spectrum: testing concrete code, in extreme cases individual lines. A unit test (also module test or component test) is a very basic test of a single concrete functionality or function or method (unit).
Characteristics and principles
What makes unit tests and distinguishes them from other types of tests? The basic characteristic of unit tests is their isolation. A unit test executes only one concrete function and excludes all external influences (communication with other systems, calls to other functions, etc.). As central keywords for the warranty of this isolatingness here e.g. mocking as test-sided technology as well as parameterization and the principle of the simple responsibility as programming methodologies with the development of the testable code are mentioned. Incidentally, it is this isolation (and thus the decisive characteristic of a unit test) that is often greatly underestimated and neglected in my experience. There are developers who are not aware of the consequences of a non-existing isolation. The surprise is then great when a small change in code A suddenly causes the tests of code C, D, E and K to fail.
Unit tests unfold their potential for ensuring the correctness of functionalities particularly when not only so-called good cases are tested, but also explicit misbehavior. These are referred to as special or borderline cases. In test-driven development (TDD), however, this principle is somewhat neglected. The challenge consists in particular of first recognizing borderline cases as such and also assessing how realistic their occurrence is. Of course, it does not make sense to consider every conceivable borderline case. When using tests to analyze and correct errors/bugs that have occurred, it is precisely these error cases that are actually systematically tested in order to then be able to correct them.
Furthermore, unit tests are executed continuously or constantly to ensure correctness of the code over time. This requires both discipline on the part of the developer (who must regularly test at least his own code) and automation (usually through continuous integration). The central principle behind unit testing is: the interface, not the implementation, is tested; the focus is not on the code, but on what it does or what result it delivers.
How to use the tests in a meaningful way?
A methodology is necessary for the meaningful and fruitful use of unit tests. Such a methodological framework is provided by the aforementioned test-driven development as part of the eXtreme Programming model. In principle, it is possible to develop test-driven with all types of tests (i.e. also acceptance tests, integration tests, etc.). Here I would like to deal however only with the unit tests, as it is also frequently handled in the relevant literature.
The idea behind the concept is simple: The code to be tested is developed through productive use (in the context of a test) in the first place. So first the test is written, which uses the function just developed. The actual code is then developed on this basis.
Why are retrospective tests bad?
If you really want to develop test-driven, you write the tests before the actual code. However, the exact opposite approach is often found in practice, because on the one hand unit tests are often still seen as a necessary evil, a chore or a waste of time. On the other hand, the exact knowledge of the methodical procedure and the realization of the sense and benefit is perhaps often missing. And it is precisely in these situations that the above question may arise.
Frank brings the answer to the point in a detailed, interesting article on the subject:
Test cases derived from the code cannot detect omissions in the code! If, for example, a query was forgotten to be implemented, of course no test case can be derived from the code that checks this query. Even if 100% coverage is achieved, the test statement is doubtful, because the code was assumed to be complete.
Therefore, unit tests are developed cyclically according to the test-driven methodology. The procedure follows the pattern: test a little, code a little. A test is written that initially fails. Now, just enough productive code is implemented to make the test pass successfully. Afterwards test and productive code are refactored.
Unit tests significantly simplify and accelerate error detection and make problems visible at an early stage of development. If an error is discovered, it must necessarily be in the unit that has just been tested.
- Fast feedback through regular execution
- Concentration on the essentials
- Not only writing code, but also working with the code, i.e. constant rethinking of one’s own methodology and thus optimization of the code
- High test coverage possible
- Software design becomes automatically testable and as a rule also of higher quality
- The use of the code is documented by the tests directly, with consistent proceeding and use some documentations can be omitted
Prejudices and acceptance problems
However, meaningful, goal-oriented, methodically clean unit tests are not a matter of course. A number of challenges arise, which particularly affect the developer and his way of working. First of all, the establishment of unit tests is accompanied by the implementation of a new paradigm that calls established procedures into question. Acceptance must therefore grow.
Common acceptance problems associated with unit tests relate to the following aspects:
- Tests are often performed just for the sake of testing.
- The initial time required is almost always comparatively high.
- Web applications in particular are difficult to test (user interactivity, browser behavior, etc.).
- Unit tests are not the solution for every problem (whereas TDD does not only talk about unit tests).
- Discipline is required.
- Experience must be acquired. (Here e.g. Coding Dojos/Code Katas have proven themselves).
Basically, the execution of unit tests should be automated to ensure consistent quality over many releases. Continuous Integration probably offers the easiest way to do this.
Admittedly, test-driven development and unit tests also face limitations. Developers could use unit tests incorrectly, and there is no guarantee that they will be free of errors. There is a certain, not inconsiderable initial outlay, which must be taken into account in the costing of software projects.
And last but not least, it must be clear that unit tests are only suitable for pure functional testing; performance, usability and other factors cannot be measured with the help of unit tests.
Unit tests are not only useful as part of the development process, but also for error correction. The following principle applies: one test per bug. Experience shows that unit tests are helpful in bug fixing, both in isolating the bug and in fixing it.
Thus, unit tests are powerful tools that have established and proven themselves especially in iterative software development. On the developer side, a rethinking and the internalization of new procedures are certainly necessary. However, these are in any case in the sense of the highest possible code quality and in particular the iterative extensibility of the software with as little friction as possible.