We can define the testability of a system or component as
Viewed in the abstract, software components take inputs (parameters, messages, data structures, files) compute internal state, and generate outputs (calls, messages, updated data structures, and files). In software testing, we generate inputs and measure outputs, checking to see that the outputs are what we would have expected (as a function of the supplied inputs). In more practical terms, saying that a component is testable means:
The bottom line is that we can, relatively simply and economically gain considerable confidence about a component's correctness. This is clearly a good thing. But what characteristics of a system or component make it testable?
How easy is it to capture the outputs of a component, and to what extent do these observable outputs permit us to infer the correctness of the computation?
Given an input and an output, we can easily determine whether or not the component properly processed the supplied input.
It may be a little more work to capture messages that are sent and received by the application, but given the inputs, outputs, and all of the messages exchanged, we should be able to determine whether or not the component properly processed the supplied inputs.
There could be many errors in the component's internal state that would not manifest themselves in any particular output.
If we were not able to capture the input or output messages, then we would not be able to observe enough key events to enable us to infer whether or not the component was working correctly. These key events would not be observable to us.
If we had a way of dumping out the internal state of the component after each transaction, we could ascertain whether or not the internal state had been properly updated. If the internal state was not reasonably accessible to our testing tools, then this state would not be observable to us.
If all key outputs, events, and state of a component can be observed by a testing framework, then it should be possible to determine whether or not the component is functioning correctly. If this is not the case, there may be unobservable errors hiding within the unexposed state.
When a statefull component, or one with complex outputs is being designed, thought must be given to how the state and outputs can be made accessible to testing software.
How easy is it to drive the inputs of a component, and to what extent do these controllable inputs permit us to fully exercise its capabilities.
If a component has sophisticated interactions with other components, we need a plan for simulating a wide range of behaviors on the part of the other components. To the extent that we can do that, those inputs become controllable.
If a component operates on the basis of accumulated state, we need a plan for how we can put the component into any particular initial state, so that we assess its behavior to subsequent events. To the extent that we can do this, that initial state becomes controllable.
Would you know correct behavior if you saw it?
This almost has to be a joke? Right?
Some people attempt to design systems without a clear definition of correct behavior. They do so on the assumption that they can then tune (or debug) the system until its behavior is satisfactory. This approach works for training neural networks, but it is not a good plan for a designed system.
Having clear definitions of correct behavior (in this state, given these inputs, the system will xxx):
It is even better if the definition of correctness is a concise one:
Is it possible to build and test each function in isolation, or do we have to exercise everything to test anything?
If nothing works until everything works, we will find testing and debugging to be a painful process:
It is much better if each routine or class can be exercised as soon as it is written, and before it is combined with other pieces.
If we can control the inputs and observe the outputs of a single function, it is relatively easy to figure out what that function did. If we can only observe the operation of the entire program, it may be very difficult to figure out where an incorrect result came from.
There may be some components that cannot be tested in perfect isolation, but if there is a partial ordering of functionality testing that enables us to test all of the other sub-components first, we can be relatively sure that new problems are in the new code.
A testable system is one about which we can relatively simply and economically gain considerable confidence about its correctness. Testability is a characteristic of both good architecture and good design. It is right up there with modularity in terms of its impact, but unfortunately it does not seem to receive much attention.
Testability follows directly from a few easily assessed properties a design, and this means that it is within our power to create components that are highly testable. More testable components mean that we can more easily gain greater confidence of their correctness. As a pleasant side effect, components developed with a clear definition of correctness are more likely to be correct in the first case.