What is TDD?
Test Driven Development (TDD) is a technique that focuses on writing a test before writing code to pass the test. More traditional techniques would put writing code for a product first and then writing the test to see if the code previously written is performing as intended.
While the efficacy of these two techniques can be debated, with both sides producing their own positives and negatives, I intend to show the structure of TDD and provide how it can create a workflow that leads to leaner code with fewer bugs at the end of a product's development cycle.
Test Driven Development follows a cycle of understanding the request, writing a test, writing code to pass the test, refactoring your code, and then repeating this until you have your product.
This figure is sourced from Grant Steinfeld's article on Test Driven Development: https://developer.ibm.com/articles/5-steps-of-test-driven-development/
With a focus on test writing, it may seem that this increases the time it takes to reach a final product. This is true, but if you consider making patches to your code after launch, studies have shown that teams using Test Driven Development techniques spend less time at this stage because there are fewer bugs and their code base is leaner. If you want to learn more about these studies, I highly recommend reading Ben Aston's article "Statistics & Studies: The Benefits Of Test Driven Development". Read here.
Understand the Request
While this part of the process is not usually referenced when looking into TDD, I appreciate Grant Steinfeld's inclusion in his article.
By taking time to digest the request before you code, TDD becomes easier to begin and implement. The fine details are not as important as being able to talk about the project and the outputs that are expected. Consider making a loose plan as well to keep workflow focused.
Let's consider a request for creating a doubly linked list data structure. This topic has been chosen for its familiarity with most developers.
Request: "I need a class that creates a doubly linked list for storing data. The class needs methods to add data to the head or tail as well as remove data from the head or the tail. There should also be a method to check if a given piece of data is contained in the list. I have attached an image of what I am expecting."
At this point, you should take time to think about the request. We have a class that needs methods to add and remove data. This data may need to be stored as an object to create the correct connections between the data. Now that we've taken time to think, we'll begin to code.
Write the Test
Before coding for the request, write a test that would make your code fail the test. This performs the two tasks of confirming your test code is functional (by failing and not automatically passing) and creating a goal to reach.
There are a couple of ways to write a test that will fail:
Write a test that references a function or property that doesn’t exist yet.
Write a test that expects a certain value to be returned (that isn’t already being returned).
Typically, you'll want to begin writing your first test before you begin to code anything for the request. You'll notice that the first two tests are about making sure we have certain properties and methods in our doubly linked list class. For this article, we'll focus on the third test and beyond.
describe('TDD Example -', function() {
let doublyLinkedList;
beforeEach(function() { // Perfrom this code before each test.
doublyLinkedList = new DoublyLinkedList();
});
// Test #1: Properties
it('should have a head and tail', function() {
expect(doublyLinkedList).to.have.property('head');
expect(doublyLinkedList).to.have.property('tail');
});
// Test #2: Methods
it('should have methods named addToTail, removeHead, contains, addToHead, & removeTail', function() {
expect(doublyLinkedList.addToTail).to.be.a('function');
expect(doublyLinkedList.removeHead).to.be.a('function');
expect(doublyLinkedList.contains).to.be.a('function');
expect(doublyLinkedList.addToHead).to.be.a('function');
expect(doublyLinkedList.removeTail).to.be.a('function');
});
// Test #3: Adding data to the list at the tail.
it('should designate a new tail when new nodes are added to tail with addToTail', function() {
doublyLinkedList.addToTail(1); // Add data
expect(doublyLinkedList.tail.value).to.equal(1); // Check for data
doublyLinkedList.addToTail(2); // Add data
expect(doublyLinkedList.tail.value).to.equal(2); // Check for data
doublyLinkedList.addToTail(3); // Add data
expect(doublyLinkedList.tail.value).to.equal(3); // Check for data
});
This is a test to check that data is being added to the tail property. I have included a property called value to be sure that I'm getting the data I added to the tail when I check for it. The test fails because the tail of my list points to null instead of any meaningful data. Now it's time to code!
Make it Pass
Once you have the failing test, write code to pass the test. Keep it simple. Trying to think of every situation will distract you from the task at hand: passing the failing test. The code you write at this stage does not have to be elegant, so your goal should be to write as little code as possible.
At this stage, you want to focus on writing as little code to pass the failing test you wrote into your test suite.
class DoublyLinkedList {
constructor() {
this.head = null;
this.tail = null;
}
addToTail(value) {
// Create a class to store the value of the data.
const newDoublyNode = new DoublyNode(value);
// Reassign the tail of the list to the new node.
this.tail = newDoublyNode;
}
removeHead() {}
contains() {}
addToHead() {}
removeTail() {}
}
// Create a class to remember the value of the data stored in the list.
class DoublyNode {
constructor(value) {
this.value = value;
}
}
Congratulations! The test is now passing and we are one step closer to delivering a product that meets the expectations of our requester. Onto the refactoring stage.
Refactor Code
Since the goals in previous iterations of the TDD cycle were to get a test to pass, the code base may become less efficient when it comes to time complexity, less organized and readable, and perhaps the code may stop following best practices. In regards to the above example, we did not write much code so the refactor stage is not warranted, but you should still consider this stage even when you write only a little code because you may be able to improve upon what you just created or have created before the test.
Refactoring code after each test passes gives you the chance to engage with your code base, improve its implementation, and think of new ideas to improve upon the initial request without losing focus on the product you are producing. By having a wealth of tests in the test suite, every change made must adhere to the structure being developed with each cycle of TDD, which leads to fewer bugs when the project is completed as well as faster debugging when the need arises.
This figure is sourced from BEN ASTON's article "Statistics & Studies: The Benefits Of Test Driven Development: https://thectoclub.com/general/statistics-studies-benefits-test-driven-development/
The refactoring stage may be more involved because as you write new code or improve code that was written earlier, some of your older tests may fail.
After finishing this cycle of TDD for this example, you may have several ideas to improve what I wrote: When you replace the tail each time, doesn't the previous tail get garbage collected? Shouldn't each piece of data have connections to the data next to it in the list?
These are great questions and exactly what fuels Test Driven Development to be great for workflow.
Repeat
After writing a test, passing the test, and refactoring code, you begin the cycle again by writing a new failing test that either improves upon the previous code written or introduces a new feature to the project.
With every loop of Test Driven Development, the design and functionality of your code become leaner, the breadth of your test suite covers move details to catch bugs more quickly, and your understanding of the project becomes more familiar.
“This workflow is sometimes called Red-Green-Refactoring, which comes from the status of the tests within the cycle.” –Grant Steinfeld
Let's follow those questions from earlier and write a test to check that the nodes are connected in some way to avoid being garbage collected unnecessarily.
it('should return the value of the node previous of the tail', function() {
doublyLinkedList.addToTail(1);
doublyLinkedList.addToTail(2);
expect(doublyLinkedList.tail.previous.value).to.equal(1);
// Add more nodes and test again
doublyLinkedList.addToTail(3);
doublyLinkedList.addToTail(4);
doublyLinkedList.addToTail(5);
expect(doublyLinkedList.tail.previous.value).to.equal(4);
});
When observing the tests I wrote, you'll notice I included a "previous" property on the data node. I wrote this because I intend for the data node itself to contain the connection between data nodes just like the picture shown in the request. The test fails because my data nodes have no "previous" property.
In order to wrap up this article (and to not keep you for the many iterations of the Test Driven Development cycle needed to complete this request), I will stop my example here. Feel free to use this starting point to create your own Doubly Linked List. Practicing the Test Driven Development cycle makes this approach less intimidating, and I am certain that you'll appreciate the result of the product you produce by the end.
Conclusion
Test Driven Development encourages programmers to engage with their code base more mindfully since they are expanding the test suite while they develop their code for a request. When considering this technique, do not let the idea of a longer development timeline dissuade you from trying. The reports of saving time after a product launch should be encouraging because you will have more time to work on future projects. In Ben Aston's research on Test Driven Development, he noted that "as programmers get better at TDD, they are likely to move faster."
Consider Test Driven Development on your next project!
Happy Coding!
Tyler Meyer
Sources:
5 Steps of Test-Driven Development by Grant Steinfeld:
https://developer.ibm.com/articles/5-steps-of-test-driven-development/
Statistics & Studies: The Benefits Of Test-Driven Development by Ben Aston:
https://thectoclub.com/general/statistics-studies-benefits-test-driven-development/
Top comments (0)