How to actually do TDD: a practical example
In today’s article, we will talk about test-driven development (TDD), as this is a highly popular discussion topic in the software development community.
Some people swear by it and insist you cannot produce any quality software without it. Others say that TDD is severely overhyped or even useless altogether. Some even resort to referring to TDD practitioners as cult members.
What’s interesting is that many people on both sides don’t even understand what TDD is. Very often, they aren’t even arguing against each other, but rather against what they think the people on the other side mean.
This is not surprising. While the internet is full of articles on TDD, most of them are either very vague or even incorrect. Therefore, to alleviate misunderstanding, I will attempt to explain what TDD is by providing a simple, but realistic example in pseudocode.
I’ve chosen pseudocode and not any concrete programming language so anyone can benefit from the information and not only the people who practice the said language.
Also, before we begin, here is a disclaimer. I am not taking sides. I am neither pro- nor anti-TDD. My job is merely to tell you what TDD is so you can decide for yourself whether you want to use it or not.
So let’s begin.
What TDD is not
Before we can start, let’s address some popular misconceptions about TDD.
TDD is not about writing all your tests first
Writing all your tests upfront is a tedious process when you don’t have any implementation in the code yet. Also, before you finish writing them, you may find that your original understanding of the requirements was not 100% correct. Therefore, you have to rewrite some of your tests.
This is one of the reasons people quote that TDD doesn’t work. However, this practice isn’t even TDD.
TDD is not synonymous with unit testing
Sometimes, when you observe an argument between a TDD proponent and its opponent, it becomes very obvious that they aren’t even arguing about TDD. They are arguing about the utility of unit tests per se and either one of them or both of them assume that TDD is nothing but a practice of having your code covered with unit tests.
This is also not what TDD is. You can have your code covered by unit tests and even have 100% coverage without practicing TDD.
What TDD is
In TDD, you don’t just write your tests before you write your code. You use your tests to drive your code. This distinction will make sense once I outline the TDD process, which is as follows:
Write all test cases for the behavior (a single behavior) you want to implement
Start turning one of the test cases into a concrete executable test
Add the implementation code to make the test pass
Refactor the code as needed
Repeat for all remaining test cases
This list of steps on its own will not make much sense to you, especially as steps 2 and 3 may be repeated multiple times for the same tests (you will see in the minute how). So let’s see how these steps are applied in a concrete example.
Problem statement
Imagine that we need to build a library that converts markdown text into HTML. This is something you may come across. For example, this is what GitHub does when you view the README files in the browser.
So, let’s see how we can solve this problem by applying the TDD approach.
Step 1: writing test cases
While looking at the requirements, we can probably come up with the following test cases:
The library should handle the empty input
The library should wrap all paragraphs in the
<p>HTML tag.The library should wrap all lines that start with
#into the<h1>tag (same goes for##and<h2>,###and<h3>, etc.)The library should replace any other special characters with the appropriate HTML tags
The library should be able to handle large inputs
Looking at this list, we can see that we may merge test cases 2, 3, and 4 into a single test case. We can write a test that contains all special markdown characters in the input and verify that it has been correctly converted into the HTML.
In a real-life scenario, this may not be appropriate, as the input may be hard to read, but in our example, we will do it to make it easier to demonstrate the concepts. Otherwise, this article would end up being very long.
So, here is our final list of test cases:
The library should handle the empty input
The library should add HTML tags where appropriate and substitute any special markdown characters with the appropriate HTML tags
The library should be able to handle large inputs
IMPORTANT: If the library that we are building includes more than one behavior, we forget about the other behaviors for now. We just focus on one behavior and only outline test cases for this particular behavior.
For example, the intended functionality of our library might be to translate both ways between markdown and HTML. We don’t care about the other functionality for now. We can come back to it when we have implemented teh current functionality. This will minimize context switching.
We are now ready to start our development process.
Step 2: converting test cases into tests
So, let’s pick up the first test case, which is as follows:
The library should handle the empty input
We are writing our library from scratch. Therefore, we neither have the code nor the tests.
In TDD, we start by writing a test. So, we create the test library and add a class that will contain our tests. For example, we can call it MdToHtmlConterterTests.
We can then add a test method to this class that will validate that the library can handle an empty input. Initially, we will write the method definition, which may look as follows:
CanHandleEmptyInput()
{
}However, there is not much we can do after this, as there is no code that yet exists. Of course, we can try to add an invocation to the test proactively, so it looks like this:
CanHandleEmptyInput()
{
Assert.Empty(MdToHtmlConverter.Convert(""))
}Many TDD purists will even encourage you to do this. However, this code will not even compile. Moreover, by writing it, you will be fighting against your code editor. It will probably have autocomplete functionality, which may try to replace your non-existent invocation with something else.
So, it would be a good idea now to create the actual implementation, but only initially populate it with enough code to make your tests compile. So, let’s imagine that we have created the library, added the static MdToHtmlConverter class to it, and populated it with the following stub method:
string Convert(string input)
{
return input
}We can now come back to our test library, reference the newly created implementation library from it, add the appropriate namespace references to the test class, and complete our test to look as follows:
CanHandleEmptyInput()
{
Assert.Empty(MdToHtmlConverter.Convert(""))
}This is more than sufficient for the base case. So now, let’s have a look at the second test case:
The library should add HTML tags where appropriate and substitute any special markdown characters with the appropriate HTML tags
Now, we may specify the next test method as follows:
CanApplyConversion()
{
string input = "This is the first paragraph. It has * and *.\r\n" +
"This is the second paragraph. It has ** and **."
string expectedOutput = "<p>This is the first paragraph. It has <i> and </i>.</p>\r\n" +
"<p>This is the second paragraph. It has <b> and </b>.</p>\r\n" +
"<br/>\r\n"
string actualOutput = MdToHtmlConverter.Convert(input)
Assert.Equal(expectedOutput, actualOutput)
}Of course, this is an overly simplified example. In a real example, we would have an input that contains all possible markdown characters and an expected output that has HTML tags in their correct places. But let’s keep this for the simplicity of demonstration.
This time, there is nothing that prevents us from writing the entire test. The code will compile. However, if we run it, the test will fail. This is because the Convert() method doesn’t have any conversion logic in it. It merely returns the original value of the input.
So now, we will use this test to drive the necessary behavior change. We will apply some logic to our Convert method, so it will look like this:
string Convert(string input)
{
if (input is empty)
return input
Split into paragraphs
For each paragraph:
If the line starts with a # symbol
Wrap it in the appropriate <hX> tag where X is the number of # symbols
Remove the original sequence of the # symbol
Else
Wrap each paragraph in the <p> tag
Find one of the special markdown characters
Replace them with opening and closing HTML tags
}It doesn’t even have to be elegant. We just need to make sure that our code works and our test passes. We can make it more elegant when our test passes.
So we are now left with the final test case:
The library should be able to handle large inputs
So, we may create another test method where we construct a very long input string that occupies several megabytes in size. We can then add some assertions to see that it’s still processed in a reasonable amount of time.
Perhaps, we can add a timer and make it throw exception when a specific amount of time elapses before the test finishes executing.
This time, our original code may still work. However, we may also find that it doesn’t. In this case, we will need to optimize our implementation. This is where we may have to replace the original brute force approach to solving this problem with some sort of algorithmic solution.
If you ever did Leetcode, you would be familiar with this kind of problem. It’s not uncommon to see your solution pass a couple of basic test cases only to find out that it times out with more complex sets of inputs.
Conclusion
As you can see, TDD may not be so bad after all. And in case someone says that this approach isn’t TDD, let’s see how it compares with the TDD canon.
There are two influential TDD evangelists that I will refer to: Robert Cecil Martin (commonly known as Uncle Bob) and Kent Beck. Let’s first start with what Kent Beck describes as TDD canon, which is the following:
It contains the following steps:
Write a list of the test scenarios you want to cover
Turn exactly one item on the list into an actual, concrete, runnable test
Change the code to make the test (& all previous tests) pass (adding items to the list as you discover them)
Optionally refactor to improve the implementation design
Until the list is empty, go back to #2
As you can see, these are literally the same steps we outlined.
Now, let’s look at the three rules of TDD as outlined by Uncle Bob:
You are not allowed to write any production code unless it is to make a failing unit test pass.
You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
We started with the tests, as outlined in the rule 1.
We stopped writing the test as soon as we could no longer make it compile, as outlined in rule 2.
We added implementation to make the test pass, as outlined in rule 3.
So, ladies and gentlemen, this is what TDD is. Whether it’s useful or not, it’s up to you to decide. But at least you know what it’s supposed to be and how to do it.



TDD relies on smarts, experience and being able to test the corner cases of the requirements, amongst the complexity which already exists. It also requires an understanding of the business domain, the technical constraints / opportunities of the code / environment you're working with.
TDD isn't a silver bullet. It's like Agile ... having a healthy attitude and frame work towards achieving a sensible solution only works when the team are engaged and on-board.