This software, where ...
- The system consists of several subsystems
- Each subsystem consists of several components
- Each component is implemented using many classes.
... I like to write automatic tests of each subsystem or component.
I do not write a test for each internal component class (except that each class contributes to the component’s public functionality and, therefore, is tested / tested externally through the component’s public API).
When I reorganize the implementation of a component (which I often do as part of adding new features), so I don’t need to modify any existing automatic tests: since the tests depend only on the public API of the component and the public APIs usually expand rather than change.
I think this policy contrasts with the document, for example Test code refactoring , which states that ...
- "... unit testing ..."
- "... a test class for each class in the system ..."
- "... test code / production code ... perfect for approaching a 1: 1 ratio ..."
... all of which, I believe, do not agree (or at least not practice).
My question is: if you do not agree with my policy, explain why? In what scenarios is this degree of testing insufficient?
In short:
- Public interfaces are tested (and retested) and rarely changed (they are added, but rarely changed)
- Internal APIs are hidden behind public APIs and can be changed without overwriting test cases that test public APIs
Footnote: Some of my “test cases” are actually implemented as data. For example, test cases for the user interface consist of data files that contain various user inputs and the corresponding expected system outputs. System testing means having a test code that reads each data file, repeats the input to the system and claims that it receives the corresponding expected result.
Although I rarely have to change test code (because public APIs are usually added rather than changed), I have found that sometimes (for example, twice a week) I need to modify some existing data files. This can happen when I change the system output for the better (that is, new functions improve the existing output), which may lead to the existing test failing (because the test code is only trying to assert that the output has not changed). To handle these cases, I do the following:
- Restart the automated test suite, which uses a special run-time flag that tells it not to approve the output, but instead write the new output to a new directory
- Use the visual demarcation tool to see which output files (for example, which test cases) have changed, and to make sure that these changes are good and expected with the new functionality
- Update existing tests by copying the new output files from the new directory to the directory from which the test cases are executed (rewriting old tests)
Footnote: by “component” I mean something like “one DLL” or “one assembly” ... something big enough to be visible on the architecture or system deployment diagram, often implemented using dozens or 100 classes and a public API that consists of only 1 or more interfaces ... something that can be assigned to one development team (where another component is assigned to another team), and therefore, according to Conway Law , which has a relatively stable public API.
Footnote: An Object Oriented Testing article : Myth and Reality reads:
Myth: checking a black box is enough. If you do a thorough job of testing the design using the class interface or specification, you can be sure that the class is fully implemented. White box validation (looking at the implementation of a method for designing tests) violates the very concept of encapsulation.
Reality: The structure of the TOE is important, Part II. Many studies have shown that black-box drawers are considered painfully carefully by developers to only exercise one-third to one-half (not to mention paths or states) when implementing test work. There are three reasons for this. Firstly, the inputs or states selected usually follow the normal path, but do not force all possible paths / states. Secondly, black box testing alone cannot reveal surprises. Suppose we have checked all the specified system behavior under testing. To be sure, there is no vague behavior that we need to know if testing in any part of the system was not performed by the black box. The only way information can be obtained from the code is measuring instruments. Thirdly, it is often difficult to implement exception and error handling without checking the source code.
I have to add that I am doing whitebox functional testing: I see the code (in the implementation) and I write functional tests (which give the public API) to implement the various branches of the code (details of the implementation of the function).