Test-First Programming for Improved Software, or Revisiting Red-Green-Refactor
We use test-driven development (TDD), pair programming, and other practices and principles generally associated with Extreme Programming. Test-first programming operates from a fundamentally different thought process compared to approaches that test after writing code (or that don’t really test at all!). Effectiveness in test-first programming requires a mastery of that thought process.
The traditional cycle of TDD, commonly referred to as Red-Green-Refactor, has the following steps:
Red: Write a failing test.
Green: Make the failing test pass.
Refactor: Improve the implementation while maintaining passing tests.
The refactoring step tends to receive the least attention, so Red-Green-Refactor often becomes Red-Green. Without ongoing attention to improving the design of the system, much of the potential value of test-first programming is lost.Given strong testing through the Red and Green phases of Red-Green-Refactor, why do we often not observe abundant energy and confidence for the Refactor phase?
In revisiting the fundamental feedback loop of TDD, it is useful to consider its fundamental nature.
What is test-driven development? The name clearly states that tests are written first, but this brings up further questions. What kinds of tests are written first? What sort of test can drive the development of a system? How can something be tested before it exists?These are natural areas of potential confusion, and we encounter them regularly in our work with software engineering teams of varying testing persuasions and practices.
Verification of expected results is perhaps what comes to mind most readily when thinking about the purpose of testing.This may be the primary obstacle to effective adoption of test-first programming. If the purpose of a test is to verify expected behavior, there is an inherent conflict in the idea of defining the test before implementing the behavior.
What if the tests of test-first programming could be about something other than verification? The foundational literature of Extreme Programming develops the idea of testing as a means for evolving the design of a system. Before a test can be about verification of expected behavior, it must serve as a description of knowledge we have about a system.
Consider a reworded cycle of TDD, based on the value to be gained from the particular activities:
Counterfactual: Define new knowledge we want to be true of the system.
Factual: Make that new knowledge accurate.
Clarification: Improve the design of the system and of our knowledge of it.
Counterfactual-Factual-Clarification lacks the pithiness of Red-Green-Refactor, but perhaps it can focus our attention on the concrete outcomes to be achieved throughout development activities.
For a test suite to serve as meaningful knowledge of the system, assertions must define specific bits of knowledge. For specific bits of knowledge to be accurate, the system must be composed of parts having sufficient clarity. For an evolving system to possess sufficient clarity, the design of the system must be improved constantly.
Defining new, counterfactual knowledge of the system
Tests that repeat existing knowledge of the system decrease the usefulness of that knowledge. Repetition of knowledge about the system means that multiple sources can be cited as authoritative. It increases the difficulty of evolving the system. It hints at a lack of confidence in assertions made. It suggests that an underlying ambiguity or concern has not been addressed adequately. It may be an oversight caused by poorly defined structure of existing knowledge.
How might we define new knowledge of a system?
● It can be summarized in natural language
● It is falsifiable, and it begins false
● It exercises the system directly
● It advances our understanding of the system
● It specifies the required nuance,and no more
● It refines our understanding of the system as a whole
● It is replaced when new knowledge supersedes it
● It is easy to read and can be verified quickly
Making knowledge of the system factual
Armed with specific new knowledge of the system, we can make adjustments to the system to cause that knowledge to be accurate. Knowledge is advanced gradually, so operating in small increments of defining knowledge and making it factual enables focused implementation.Writing the test first shows us where to start, and the mechanism of running the test enables us to demonstrate completion.
How might we make new knowledge of a system become factual?
● Add implied or specified new capabilities to the system
● Minimize the gap to bridge between the factual and the counterfactual
● Make factual knowledge become counterfactual for verification
● Replace default values and behaviors with specific factual claims
● Refine existing factual claims to make the new counterfactual claims true
Clarifying the design of the system and of our knowledge of it
Having defined new knowledge of the system and updated the system itself, we proceed to identify and make useful clarifications. We can clarify the knowledge of the system by grouping bits of knowledge so that they are easily discover able in subsequent tasks of learning and extending the capabilities of the system. We can clarify the system itself by exercising a range of refactoring techniques so that the system is more easily understood and maintained.
Without some forcing function to reduce the incidental complexity of the system, incidental complexity will dominate all other factors and lead a system inexorably toward a painful rewrite (likely subject to the same trajectory). Test-first programming gives us such a forcing function in refactoring of code and clarification of tests.
How might we clarify the system?
● Consolidate overlapping knowledge
● Separate unrelated things
● Group related things
● Reduce the knowledge required
● Reduce the set of knowable things
● Surface previous gaps in knowledge
● Introduce simplifying organizing principles
● Extract implied concepts
● Upgrade existing concepts
● Separate concepts from their use
● Differentiate interaction from information
● Make knowledge more discoverable
● Improve the names of things
Conclusion: Test-First Programming for System Evolution and Emergent Design
We practice test-first programming because it enables us to improve the design of the system every day. We start by defining new knowledge. We bring the system into alignment with that knowledge. We clarify the system and the knowledge we have of it.
Through consistent application of these habits, we are able to adapt systems to accommodate new requirements, even as a cohesive design emerges.
Subscribe to our monthly newsletter to get notified about future articles.