Failing Test for Minimal Functionality Increment

My recent work project has been a source of many insights. This week, I've realized that Test-Driven Development (TDD) can be scaleless if we tweak its fundamental principles a bit.

In my understanding, TDD traditionally focuses on writing the code for a single executable process. It often does not consider interactions between multiple services or front and back ends. This leads to many of its "principles" being quite categorical and tied strictly to this "inside-the-process" scale.

However, I believe we can identify a basic set of "principles" and soften their formulations. After that they can be applied at any scale, for example the scale of user stories. Interesting, isn't it? So, let's dive into the details and explore the main principle of TDD first.

TDD Main Rule

The key rule of TDD is to have a failing test before making code changes. This test helps us define the required behavior. Moreover, it also allows us to hypothesize about the interfaces needed for its implementation. This behavior fixation aids future refactoring, while the interface hypothesis stimulates discussion right now, before any code is written. For instance, at the level of individual classes, we might ask, "Should we pass this object as an argument to this method?" On the level of user stories, we might question "Should we link these two entities at the previous stage of the process?"

At higher levels, the test can be a system concept or use case, but the essence remains - we must first have a fixed description of the desired outcome and behavior. Ideally, this description should be in a form of an automated test. This approach not only provides us with unambiguous requirements but also enables rapid feedback on our progress. It's important to note that these tests are often temporary. As we move forward with small increments, the majority of such intermediate tests will be either deleted or rewritten to accommodate the evolving functionality. This iterative process is a key aspect of maintaining agility and adaptability in our development workflow.

The main TDD rule has a fine print addition often overlooked - the requirement to move in very small steps. This is usually justified by the ease of error detection - if a test fails after a small change, the error is caused by exactly this small change. Finding an error in three lines of code is much easier than in five new 100-line classes. But that's not all the benefits of small steps. Tiny, atomic changes allow you to check all the assumptions underlying the code, as each requires writing a test, and each test is a hypothesis and a discussion of this hypothesis.

An Example of Minimal Increments

Now, let's consider an example of the user story for a cloud platform dashboard application:

User Story: As a client, I want to be able to change my cluster configuration by selecting a number of virtual machines and specifying their characteristics so that I can customize my cloud resources according to my needs.

This user story can be broken down into smaller increments as follows:

  1. When client clicks on "Change Cluster Configuration" button, they have their configuration changed to a predefined (hard-coded) configuration.

  2. When client clicks on "Change Cluster Configuration" button, they can see the list of preconfigured virtual machines and select one of them, thus changing their cluster configuration to hold only this virtual machine.

  3. When client clicks on "Change Cluster Configuration" button, they can see the list of preconfigured virtual machines and select several of them, thus changing their cluster configuration to hold only these virtual machines.

  4. When client clicks on "Change Cluster Configuration" button, they can see the list of preconfigured virtual machines and select several of them, as well as specify their characteristics (e.g., CPU, memory, disk size), thus changing their cluster configuration to hold these virtual machines with specified characteristics.

Each of these user stories represents a small increment of functionality that can be developed, tested, and delivered independently. This approach allows for more frequent feedback and the ability to adjust the development plan based on that feedback.

Conclusion

Moving in small steps requires good discipline because it's a very counterintuitive practice. Personally, I find various checklists helpful in this, for example, at the unit test level, the acronym ZOMBIES works well, and at the story level - templates for breaking down stories into smaller ones.

The result for the first rule is as follows: whatever we start, we first need to formulate a way to check the result, while reducing the scope of planned work to an absolute minimum. That is, we want to implement the smallest possible increment of functionality and at the same time, we want to know exactly how we will check the correctness of the implementation. The essence of this rule does not depend on the level of consideration, will it be the code of a single class or the entire system.

0
Subscribe to my newsletter

Read articles from Evgenii Terekhov directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Evgenii Terekhov
Evgenii Terekhov

I am an enthusiastic software engineer on a quest to harness the software complexity. I am an author of the small framework that moves the complex business logic out of the code into the external configuration data. This framework is the first step in my ongoing quest.