TDD Fundamentals
In this series, I will be breaking down concepts from the famous book by Kent Beck - Test Driven Development. I will try to distill concepts further to make it easier to understand by the reader.
In this post, I will be explaining the terms and will also tell you what I think about those terms.
Test (noun)
Test is both a verb and noun. Test in terms of verb, basically means to evaluate. In the context of Software Engineering Test(verb) is basically to evaluate your software. This evaluation is nothing but if your system/software is working as you wanted it to. You must have heard this statement: Did you test it? Here the word test is used as a verb.
But Test(noun) is something that you create as part of your software to evaluate your system in an automatic manner. The test can either fail or succeed, and will tell you the current status of your system.
I found one really interesting concept in the book called Stress Death Spiral and I believe we all can relate to it. It looks something like this:

Kent has explained it in terms of a positive feedback loop. Consider a situation, where you have found a bug in production and are currently in the process of fixing it. I am sure we all have been there, and undoubtedly this situation is full of Stress.
In this situation you really want to fix the bug as soon as possible and release it out. And even though you have fixed a bug, you have either not tested it at all or didn't test it thoroughly, and in the both the situations you are more likely to make more mistakes. You perhaps only tested the bug fix but were in a hurry and didn't run your test suite, and I am with you, you were in Stress situation. But now you have a system that is not functional at all because you broke the main flow in production. You see where is it going. This will definitely create more Stress for you.
But we can break this cycle and we must or the consequences and stress can be real bad.
It is really easy to break it, simply Test it and not just test it, test it thoroughly.
- Make sure it works
- and it doesn't break anything else
Yes, it will take more time, take it and you will be grateful to yourself.
Never say that, we don't have time to run the tests, release it!
Let's also take all of this from another perspective, what if you don't have stress or have low stress. This is most likely the situation where you have started building some functionality. You may have a bit of Stress of timeline, but then again you can recall the previous experience from above. When you have less Stress, you are most likely to add more tests and test it. The more tests you add, the more confident you will become and it would reduce your Stress further.
This makes it one thing clear to me, test it from the beginning when you have low stress and build on top of it. This is the time when you are building a new functionality or adding something on top of an existing one.
I hope it gives enough motivation to write tests early in the development cycle but let's make sure this motivation is not rooted in fear but in confidence!
Isolated Test
Kent has described the isolation of tests is the biggest pain point of test failure most of the times. And I couldn't agree more, in most scenarios if you are not careful, you can have some test failures which then put the system in a broken state. And then other test fails because they didn't expect the system to be in a broken state. Moreover:
- Tests should be independent of any order - many times developer rely on running tests in a specific order. This can cause chaos. You should be able to run a subset of tests without worrying that you didn't include the prerequisite test.
- Tests shouldn't take too long to run - this can cause fatigue. On the other hand if the tests don't take long you can run them often and figure out issues before anyone else does.
It is imperative that you have some kind of logic that does the setup before each test and some kind of tear down after each test. This makes sure that the system will be in desirable state even if some test fails, because the tear down would clear up all the intermediate states and would reset the system.
Test List
Question that always come to the mind when you start with the tests
What should I test?
Let's recall what we understood in the first section - we must write tests when we have less stress, which can provide a better ground for you to write tests and which in turn will reduce your stress further.
One naive approach is for you to keep everything in your mind - well who knows how much you would remember until the end. This is a recipe for disaster.
Instead you could write everything what you want to accomplish
I have been using this method from past many years and have found this to be super useful.
According to Kent, we put the tests on the list that we want to implement and he suggests the following:
- Examples of every operation that you know you need to implement
- For the new operations, also add the null version check
- List all the Refactorings that you think you would have to do, in order to have a clean code
He also suggests that writing tests in bulk is not useful. If you think OK, now I have a list of all the tests, let me write all the tests, this is not really a good approach.
For example, when you have implemented ten tests and then you discover that the arguments need to be in opposite order as compared to your actual implementation, it creates extra overhead to also do the corrections in all of those ten tests. And then you are really far away from the green bar. And most likely an increase in stress. And the whole idea is to stay away from stress.
Better start with one test build on top of that and then move on to the next test.
Test First
Now, the question that arises here
When should I write my tests?
Well you have only two choices here, either before you write your code or after.
The best advice is to write tests before you write any code that is to be tested
Why? Well, we as a programmer our primary goal is to make some functionality work as intended. And we put testing as a secondary goal.
Let's keep remembering our goal, less stress means better testing, but it also means that less testing corresponds to more stress. When you combine everything here, i.e. you write tests afterwards and since it is not our primary goal, you didn't test the functionality completely. This means that you have done less testing, which means more stress.
But when you start writing tests first, you are giving yourself a chance to reduce your stress. As you are writing more tests to cover your functionality, you are reducing your stress further and further.
Assert First
When should you write the asserts?
Kent says, write them first.
According to him, and I agree that when we are writing tests we are trying to solve many problems at once, even if we don't have to think about the implementation.
- Where does the functionality belong?
- What should the names be called?
- How are you going to check for the right answer?
- What is the right answer?
It is too hard to solve so many problems at once. Though he highlights two problems:
What is the right answer? And How am I going to check?
He provides a nice example of how to think and go about it
Suppose we want to communicate with another system over a socket. When we're done, the socket should be closed and we should have read the string abc
testCompleteTransaction() {
...
assertTrue(reader.isClosed());
assertEquals("abc", reply.contents());
}Where does the reply come from? The socket, of course:
testCompleteTransaction() {
...
Buffer reply = reader.contents();
assertTrue(reader.isClosed());
assertEquals("abc", reply.contents());
}And the socket? We create it by connecting to a server:
testCompleteTransaction() {
...
Socket reader = Socket("localhost", defaultPort());
Buffer reply = reader.contents();
assertTrue(reader.isClosed());
assertEquals("abc", reply.contents());
}But before this, we need to open a server:
testCompleteTransaction() {
Server writer = Server(defaultPort(), "abc"));
Socket reader = Socket("localhost", defaultPort());
Buffer reply = reader.contents();
assertTrue(reader.isClosed());
assertEquals("abc", reply.contents());
}See, how cleanly and clearly we landed to a final test with small steps. When I saw this first time, I was really surprised by it. And to be honest with you I have never tried it, but I will.
Test Data
We will be figuring it out the following question here
What data do you use for test-first tests?
Short answer by Kent is that, use data that makes the tests easy to read and follow. For example if you are trying to use 1 and 2 as your input and there is no conceptual difference between them then use 1.
On the other hand if your test has to handle multiple inputs, then your test should reflect multiple inputs.
And hold on, he also gives one trick.
Never use the same constant to mean more than one thing
For example if you have plus() method and you take 2 + 2 or 1 + 1 it might be troublesome to find some trivial issues where your arguments are reversed. So instead always try to use one constant for one thing. Here the appropriate example could be 1 + 2 or 2 + 3 ...
Evident Data
Now we have the following question
How do you represent the intent of the data?
Before we move further, one thing to note here is that
We are writing tests for a human reader and not a computer, so treat them accordingly
Your tests must show clearly what was expected and what was the actual result in the test itself. Idea is to leave as many clues as possible for the next reader of the test.
Consider the following example where you are trying to check your plus method.
assertEquals(5, plus(2,3));This is one way to write the test but here no one knows how this 5 has come about
There is one other way like following
assertEquals(2+3, plus(2,3));Here you can clearly see the connection between the test input and the output or result of the plus method. This makes it easier to understand the numbers and their relation.
This is what it means by evident data in the tests, sometimes it would be easier to make and show these connections, but most of the times where you are testing complex methods it won't be that easy. And specially in those conditions it becomes more important that we make it easier for the reader to understand the test.
Final Conclusion
Test-Driven Development isn't just a technical practice; it's a psychological defense mechanism. It’s how we keep our cool when the production servers are screaming. It’s how we ensure that our "Fix" today doesn't become our "Emergency" tomorrow.
In the next few posts, I’ll be bringing TDD even closer to the real world with concrete coding examples and deep-dives into refactoring.
How do you handle stress during a production outage? Do you reach for the tests, or do you fly blind? Let me know in the comments.
Happy Testing!
Member discussion