Testing interacts with your dopamine system, so you will a small yay! every time all tests passed. This is crucial, because motivation tend to decays exponentially and to experience inevitable “crashes” after spikes.
TTD is sort of a direct consequence of type-driven (or “types first”) approach to prototyping.
Ideally, each type is associated (in an one-to-one correspondence) with each distinct concept in the problem domain, at an appropriate level of abstraction.
Abstract Data Types, which are algebraic ones under the hood (at the implementation side). are the basic building blocks. “Naked” algebraic value-types “floating around” as values are also OK.
Usually, each well-designed (orthogonal to everything else) ADT reside in its own module, with only the public interface being exported..
Such levels of abstraction within the problem domain give rise to layers of functional DSLs embedded in the programming language in use.
Notice that really good (carefully designed by mathematicians) languages allow expression the proper sum-types and exponent- (function) types directly, while the product types are records and (derived) tuples.
In a good language there is no distinction between “built-in” and user-defined types, so even Bool
or option
are “user-defined” (pre-defined) in the standard librarye.
Proper support of algebraic types and ADTs via modules is, literally, all you need. And,, of course, All You Need Is Lambda.
Serious, non-bullshit testing has several characteristics
- selftest, as in an airplane before takeoff
- “blackbox-testing”,to make sure each particular abstraction has been implemented correctly
- “glassbox-testing”, explicitly test each possible code path
- “regression” testing, to make sure you didn’t break anything with the last change
- automated “coverage” – just do it
The most important part is that, glass-box testing can be systematic, in principle, when based on types. With proper algebraic types, one systematically (and mechanically) verifies each path via a data-constructor of a sum-type, which includes lists and trees recursive types, which are almost all sums, each slot and its “selector” of a product-type , each lower and upper bounds of a bounded interval type (a machine type).
Just doing this will allow on to be much more confident about the current state of the program code.
Types not only allow to write better tests systematically, they make simple bugs impossible.
The “making illegal states unepresentable” meme (which also known as “smart constructors”) is another type-based approach.
And ultimately one never have a [modifiable] “state”, and systematically produces a fresh new value instead.
The non-bullshit TTD is about “beginning with the types” and writing the type-signatures first (a an outline of a module or a class).
First one writes down the algebraic types (the sum-types, the product-types and the function types).
For each type a representation invariant shall be specified using some mathematical notation (sets and predicates).
Based on that mathematical specification, explicit tests for each condition or property (“such that”) has to be written down even before thinking about the implementation details.
Notice that all this is variations of informal specifications and type-level and then whole interface-level resoling. This is what a high-level programming consists of – designing and prototyping at the interface level.
Just as the types have been specified (defined), one writes down the example’ of values of each type. Bounds and the “special values” (should be avoided) are the first candidates.
Then one writes “stubs” for function’s (or methods) which return some dummy values and simultaneously (at the same time) writes the tests for each function (or method) for that dummy value.