Test-First Programming for Improved Software, or Revisiting Red-Green-Refactor
By: Def Method
Tags: Software Engineering, Programming, Software Development, Technology, Test Driven Development

Test-First Programming for Improved Software, or Revisiting Red-Green-Refactor

by Paul Ort, Principal Engineer

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 discoverable 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.